464 lines
17 KiB
C#
464 lines
17 KiB
C#
#nullable enable
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using T3.Core.Model;
|
|
using T3.Core.Operator;
|
|
using GraphUtils = T3.Editor.UiModel.Helpers.GraphUtils;
|
|
|
|
namespace T3.Editor.UiModel.Modification;
|
|
|
|
internal static class InputsAndOutputs
|
|
{
|
|
public static bool RemoveInputsAndOutputsFromSymbol(Guid[] inputIdsToRemove, Guid[] outputIdsToRemove, Symbol symbol)
|
|
{
|
|
if (inputIdsToRemove.Length == 0 && outputIdsToRemove.Length == 0)
|
|
return true;
|
|
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(symbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
Log.Error($"Error getting syntax tree from symbol '{symbol.Name}' source.");
|
|
return false;
|
|
}
|
|
|
|
var root = syntaxTree.GetRoot();
|
|
|
|
if (inputIdsToRemove.Length > 0)
|
|
{
|
|
if (!TryRemoveNodesByIdFromTree(inputIdsToRemove, root, out root))
|
|
return false;
|
|
}
|
|
|
|
if (outputIdsToRemove.Length > 0)
|
|
{
|
|
if (!TryRemoveNodesByIdFromTree(outputIdsToRemove, root, out root))
|
|
return false;
|
|
}
|
|
|
|
var newSource = root.GetText().ToString();
|
|
Log.Debug(newSource);
|
|
|
|
return EditableSymbolProject.RecompileSymbol(symbol, newSource, true, out _);
|
|
|
|
static bool TryRemoveNodesByIdFromTree(Guid[] inputIdsToRemove, SyntaxNode root, [NotNullWhen(true)] out SyntaxNode? rootWithoutRemoved)
|
|
{
|
|
var nodeFinder = new NodeByAttributeIdFinder(inputIdsToRemove);
|
|
var newRoot = nodeFinder.Visit(root);
|
|
|
|
rootWithoutRemoved = newRoot.RemoveNodes(nodeFinder.NodesToRemove, SyntaxRemoveOptions.KeepNoTrivia);
|
|
return rootWithoutRemoved != null;
|
|
}
|
|
}
|
|
|
|
private sealed class NodeByAttributeIdFinder(Guid[]? inputIds) : CSharpSyntaxRewriter
|
|
{
|
|
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
|
|
{
|
|
if (node.AttributeLists.Count > 0) {
|
|
var attrList = node.AttributeLists[0];
|
|
var searchedNodes = (from attribute in attrList.Attributes
|
|
from id in _ids
|
|
where attribute.ToString().ToLower().Contains(id.ToString().ToLower())
|
|
select attribute).ToArray();
|
|
|
|
if (searchedNodes.Length > 0)
|
|
{
|
|
NodesToRemove.Add(node);
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
private readonly Guid[] _ids = inputIds ?? Array.Empty<Guid>();
|
|
public List<SyntaxNode> NodesToRemove { get; } = new();
|
|
}
|
|
|
|
private sealed class InputNodeByTypeFinder : CSharpSyntaxRewriter
|
|
{
|
|
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
|
|
{
|
|
if (!(node.Declaration.Type is GenericNameSyntax nameSyntax))
|
|
return node;
|
|
|
|
var idValue = nameSyntax.Identifier.ValueText;
|
|
if (idValue == "InputSlot" || idValue == "MultiInputSlot")
|
|
LastInputNodeFound = node;
|
|
else if (LastInputNodeFound == null && idValue == "Slot")
|
|
LastInputNodeFound = node;
|
|
|
|
return node;
|
|
}
|
|
|
|
public SyntaxNode? LastInputNodeFound { get; private set; }
|
|
}
|
|
|
|
private sealed class OutputNodeByTypeFinder : CSharpSyntaxRewriter
|
|
{
|
|
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
|
|
{
|
|
if (!(node.Declaration.Type is GenericNameSyntax nameSyntax))
|
|
return node;
|
|
|
|
string idValue = nameSyntax.Identifier.ValueText;
|
|
if (idValue == "Slot" || idValue == "TimeClipSlot") // find general way to support all output types here
|
|
LastOutputNodeFound = node;
|
|
|
|
return node;
|
|
}
|
|
|
|
public SyntaxNode? LastOutputNodeFound { get; private set; }
|
|
}
|
|
|
|
private sealed class ClassDeclarationFinder : CSharpSyntaxWalker
|
|
{
|
|
public override void VisitClassDeclaration(ClassDeclarationSyntax node)
|
|
{
|
|
ClassDeclarationNode = node;
|
|
}
|
|
|
|
public ClassDeclarationSyntax? ClassDeclarationNode;
|
|
}
|
|
|
|
private sealed class AllInputNodesFinder : CSharpSyntaxRewriter
|
|
{
|
|
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
|
|
{
|
|
if (!(node.Declaration.Type is GenericNameSyntax nameSyntax))
|
|
return node;
|
|
|
|
string idValue = nameSyntax.Identifier.ValueText;
|
|
if (idValue == "InputSlot" || idValue == "MultiInputSlot")
|
|
{
|
|
var first = node.Declaration.Variables[0];
|
|
var id = first.Identifier.ValueText;
|
|
InputNodesFound.Add((id, node));
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
public List<(string, SyntaxNode)> InputNodesFound { get; } = new();
|
|
}
|
|
|
|
private sealed class AllInputNodesReplacer : CSharpSyntaxRewriter
|
|
{
|
|
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
|
|
{
|
|
if (node.Declaration.Type is not GenericNameSyntax nameSyntax)
|
|
return node;
|
|
|
|
var idValue = nameSyntax.Identifier.ValueText;
|
|
if (idValue == "InputSlot" || idValue == "MultiInputSlot")
|
|
{
|
|
return ReplacementNodes[_index++];
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
private int _index;
|
|
public SyntaxNode[] ReplacementNodes =[];
|
|
}
|
|
|
|
public static bool AddInputToSymbol(Guid inputId, string inputName, bool multiInput, Type inputType, Symbol symbol)
|
|
{
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(symbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
Log.Error($"Error getting syntax tree from symbol '{symbol.Name}' source.");
|
|
return false;
|
|
}
|
|
|
|
var root = syntaxTree.GetRoot();
|
|
|
|
var inputNodeFinder = new InputNodeByTypeFinder();
|
|
var blockFinder = new ClassDeclarationFinder();
|
|
|
|
if (inputNodeFinder.LastInputNodeFound == null)
|
|
{
|
|
blockFinder.Visit(root);
|
|
if (blockFinder.ClassDeclarationNode == null)
|
|
{
|
|
Log.Error("Can't find class declaration.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
root = inputNodeFinder.Visit(root);
|
|
|
|
var @namespace = inputType.Namespace;
|
|
if (@namespace == "System")
|
|
@namespace = String.Empty;
|
|
else
|
|
@namespace += ".";
|
|
var attributeString = "\n [Input(Guid = \"" + inputId + "\")]\n";
|
|
var typeName = TypeNameRegistry.Entries[inputType];
|
|
var slotString = (multiInput ? "MultiInputSlot<" : "InputSlot<") + @namespace + typeName + ">";
|
|
var inputString = " public readonly " + slotString + " " + inputName + " = new " + slotString + "();\n";
|
|
|
|
var inputDeclaration = SyntaxFactory.ParseMemberDeclaration(attributeString + inputString);
|
|
if (inputDeclaration == null)
|
|
{
|
|
Log.Error("Invalid input declaration");
|
|
return false;
|
|
}
|
|
if (inputNodeFinder.LastInputNodeFound != null)
|
|
{
|
|
root = root.InsertNodesAfter(inputNodeFinder.LastInputNodeFound,
|
|
new[] { inputDeclaration });
|
|
}
|
|
else if (blockFinder.ClassDeclarationNode != null)
|
|
{
|
|
var node = blockFinder.ClassDeclarationNode;
|
|
var classDeclaration = node.AddMembers(inputDeclaration);
|
|
root = root.ReplaceNode(blockFinder.ClassDeclarationNode, classDeclaration);
|
|
}
|
|
else
|
|
{
|
|
var theClass = root.DescendantNodes().OfType<ClassDeclarationSyntax>().First();
|
|
var classDeclarationSyntax = theClass.AddMembers(inputDeclaration);
|
|
Log.Info($"{classDeclarationSyntax}");
|
|
return false;
|
|
}
|
|
|
|
var newSource = root.GetText().ToString();
|
|
Log.Debug(newSource);
|
|
|
|
return EditableSymbolProject.RecompileSymbol(symbol, newSource, false, out _);
|
|
}
|
|
|
|
// todo - factor out common code between AddInputToSymbol and AddOutputToSymbol
|
|
public static bool AddOutputToSymbol(Guid outputId, string outputName, bool isTimeClipOutput, Type outputType, Symbol symbol)
|
|
{
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(symbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
Log.Error($"Error getting syntax tree from symbol '{symbol.Name}' source.");
|
|
return false;
|
|
}
|
|
|
|
var root = syntaxTree.GetRoot();
|
|
|
|
var outputNodeFinder = new InputsAndOutputs.OutputNodeByTypeFinder();
|
|
root = outputNodeFinder.Visit(root);
|
|
|
|
var blockFinder = new InputsAndOutputs.ClassDeclarationFinder();
|
|
if (outputNodeFinder.LastOutputNodeFound == null)
|
|
{
|
|
blockFinder.Visit(root);
|
|
if (blockFinder.ClassDeclarationNode == null)
|
|
{
|
|
Log.Error("Could not add an output as no previous one was found, this case is missing and must be added.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var @namespace = outputType.Namespace;
|
|
if (@namespace == "System")
|
|
@namespace = String.Empty;
|
|
else
|
|
@namespace += ".";
|
|
var attributeString = "\n [Output(Guid = \"" + outputId + "\")]\n";
|
|
var typeName = TypeNameRegistry.Entries[outputType];
|
|
var slotString = (isTimeClipOutput ? "TimeClipSlot<" : "Slot<") + @namespace + typeName + ">";
|
|
var inputString = $" public readonly {slotString} {outputName} = new {slotString}();\n";
|
|
|
|
var outputDeclaration = SyntaxFactory.ParseMemberDeclaration(attributeString + inputString);
|
|
if (outputDeclaration == null)
|
|
{
|
|
Log.Error("Invalid output declaration");
|
|
return false;
|
|
}
|
|
if (outputNodeFinder.LastOutputNodeFound != null)
|
|
{
|
|
root = root.InsertNodesAfter(outputNodeFinder.LastOutputNodeFound, new[] { outputDeclaration });
|
|
}
|
|
else if (blockFinder.ClassDeclarationNode != null)
|
|
{
|
|
var node = blockFinder.ClassDeclarationNode;
|
|
var classDeclaration = node.AddMembers(outputDeclaration);
|
|
root = SyntaxNodeExtensions.ReplaceNode(root, blockFinder.ClassDeclarationNode, classDeclaration);
|
|
}
|
|
else
|
|
{
|
|
var theClass = root.DescendantNodes().OfType<ClassDeclarationSyntax>().First();
|
|
var classDeclarationSyntax = theClass.AddMembers(outputDeclaration);
|
|
Log.Info($"{classDeclarationSyntax}");
|
|
return false;
|
|
}
|
|
|
|
var newSource = root.GetText().ToString();
|
|
Log.Debug(newSource);
|
|
|
|
return EditableSymbolProject.RecompileSymbol(symbol, newSource, false, out _);
|
|
}
|
|
|
|
public static bool AdjustInputOrderOfSymbol(Symbol symbol)
|
|
{
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(symbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
Log.Error($"Error getting syntax tree from symbol '{symbol.Name}' source.");
|
|
return false;
|
|
}
|
|
|
|
var root = syntaxTree.GetRoot();
|
|
|
|
var inputNodeFinder = new InputsAndOutputs.AllInputNodesFinder();
|
|
root = inputNodeFinder.Visit(root);
|
|
|
|
// Check if the order in code is the same as in symbol
|
|
Debug.Assert(inputNodeFinder.InputNodesFound.Count == symbol.InputDefinitions.Count);
|
|
var orderIsOk = true;
|
|
for (int i = 0; i < inputNodeFinder.InputNodesFound.Count; i++)
|
|
{
|
|
if (inputNodeFinder.InputNodesFound[i].Item1 != symbol.InputDefinitions[i].Name)
|
|
{
|
|
orderIsOk = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (orderIsOk)
|
|
return true; // nothing to do
|
|
|
|
var inputDeclarations = new List<SyntaxNode>(symbol.InputDefinitions.Count);
|
|
foreach (var inputDef in symbol.InputDefinitions)
|
|
{
|
|
var inputType = inputDef.DefaultValue.ValueType;
|
|
var @namespace = inputType.Namespace;
|
|
if (@namespace == "System")
|
|
@namespace = String.Empty;
|
|
else
|
|
@namespace += ".";
|
|
|
|
string? foundAttributeString = null;
|
|
foreach (var (nodeName, syntaxNode) in inputNodeFinder.InputNodesFound)
|
|
{
|
|
if (nodeName != inputDef.Name)
|
|
continue;
|
|
|
|
foundAttributeString = syntaxNode.ToString().Split("\n").First();
|
|
break;
|
|
}
|
|
|
|
var fallbackAttributeString = "\n [Input(Guid = \"" + inputDef.Id + "\")]\n";
|
|
var attributeString = foundAttributeString == null
|
|
? fallbackAttributeString
|
|
: $"\n {foundAttributeString}\n";
|
|
|
|
var typeName = TypeNameRegistry.Entries[inputType];
|
|
var slotString = (inputDef.IsMultiInput ? "MultiInputSlot<" : "InputSlot<") + @namespace + typeName + ">";
|
|
var inputString = " public readonly " + slotString + " " + inputDef.Name + " = new " + slotString + "();\n";
|
|
|
|
var code = attributeString + inputString;
|
|
var inputDeclaration = SyntaxFactory.ParseMemberDeclaration(code);
|
|
if (inputDeclaration == null)
|
|
{
|
|
Log.Error("Skipping invalid input declaration " + code);
|
|
continue;
|
|
}
|
|
inputDeclarations.Add(inputDeclaration);
|
|
}
|
|
|
|
var replacer = new InputsAndOutputs.AllInputNodesReplacer
|
|
{
|
|
ReplacementNodes = inputDeclarations.ToArray()
|
|
};
|
|
root = replacer.Visit(root);
|
|
|
|
var newSource = root.GetText().ToString();
|
|
Log.Debug(newSource);
|
|
|
|
return EditableSymbolProject.RecompileSymbol(symbol, newSource, true, out _);
|
|
}
|
|
|
|
|
|
public static bool RenameInput(Symbol symbol, Guid inputId, string newName, bool dryRun, out string? warning)
|
|
{
|
|
warning = null;
|
|
|
|
var isValid = GraphUtils.IsNewFieldNameValid(newName, symbol, out var reason);
|
|
if (!isValid)
|
|
{
|
|
warning= $"{newName} is not a valid input name: {reason}";
|
|
return false;
|
|
}
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(symbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
warning= $"Error getting syntax tree from symbol '{symbol.Name}' source.";
|
|
return false;
|
|
}
|
|
|
|
var root = syntaxTree.GetRoot();
|
|
var sourceCode = root.GetText().ToString();
|
|
|
|
var alreadyExistsResult = Regex.Match(sourceCode, $@"\b{newName}\b");
|
|
if (alreadyExistsResult.Success)
|
|
{
|
|
warning= $"{newName} is already used within the operators source code.";
|
|
return false;
|
|
}
|
|
|
|
var inputDef = symbol.InputDefinitions.FirstOrDefault(i => i.Id == inputId);
|
|
if (inputDef == null)
|
|
{
|
|
warning= $"{inputId} input definition not found.";
|
|
return false;
|
|
}
|
|
|
|
var newSource = Regex.Replace(sourceCode, @$"\b{inputDef.Name}\b(?!\()", newName);
|
|
|
|
if (dryRun)
|
|
return true;
|
|
|
|
return EditableSymbolProject.RecompileSymbol(symbol, newSource, true, out warning);
|
|
}
|
|
|
|
public static bool RenameOutput(Symbol symbol, Guid outputId, string newName, bool dryRun, out string? warning)
|
|
{
|
|
warning = null;
|
|
|
|
var isValid = GraphUtils.IsNewFieldNameValid(newName, symbol, out var reason);
|
|
if (!isValid)
|
|
{
|
|
warning = $"{newName} is not a valid output name: {reason}";
|
|
return false;
|
|
}
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(symbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
warning = $"Error getting syntax tree from symbol '{symbol.Name}' source.";
|
|
return false;
|
|
}
|
|
|
|
var root = syntaxTree.GetRoot();
|
|
var sourceCode = root.GetText().ToString();
|
|
|
|
var alreadyExistsResult = Regex.Match(sourceCode, $@"\b{newName}\b");
|
|
if (alreadyExistsResult.Success)
|
|
{
|
|
warning = $"{newName} is already used within the operators source code.";
|
|
return false;
|
|
}
|
|
|
|
var outputDef = symbol.OutputDefinitions.FirstOrDefault(o => o.Id == outputId);
|
|
if (outputDef == null)
|
|
{
|
|
warning = $"{outputId} output definition not found.";
|
|
return false;
|
|
}
|
|
|
|
var newSource = Regex.Replace(sourceCode, @$"\b{outputDef.Name}\b(?!\()", newName);
|
|
|
|
if (dryRun)
|
|
return true;
|
|
|
|
return EditableSymbolProject.RecompileSymbol(symbol, newSource, true, out warning);
|
|
}
|
|
} |