Skip to content

Instantly share code, notes, and snippets.

@frarees
Last active December 8, 2023 10:30
Show Gist options
  • Save frarees/7709014 to your computer and use it in GitHub Desktop.
Save frarees/7709014 to your computer and use it in GitHub Desktop.
Evaluate C# code on editor for Unity3D
// https://frarees.github.io/default-gist-license
using System.IO;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Linq;
public class MacroKiller : EditorWindow
{
readonly Object m_LockCompile = new Object();
static int s_MacroCount = 4;
static int s_MacroCountDefaultValue = 4;
static int s_MacroCountMinValue = 1;
static int s_MacroCountMaxValue = 8;
static string s_MacroCountEditorKey = "macrokiller_slots";
static string s_MacroKeyFormat = "macrokiller_{0}";
static string s_MacroTitleKeyFormat = "Macro {0}";
static string s_CompilerErrorPattern = @"(.*)?\((\d*),(\d*)\) : ((error|warning) (CS\d{4}): (.*))";
static string s_BaseSource = @"
namespace __macrokiller {{
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
public static class Evaluator {{
#pragma warning disable 169, 414
static object {1};
public static void Eval() {{
{0}
}} }} }}";
[SerializeField] string m_Text;
[SerializeField] string m_Output;
[SerializeField] bool m_Error;
static MacroKiller()
{
SetEnvironmentVariablesForMono();
}
static void SetEnvironmentVariablesForMono()
{
var pathEnv = System.Environment.GetEnvironmentVariable("PATH");
if (pathEnv.Contains("Mono/bin"))
{
return;
}
var monoPath = Path.Combine(EditorApplication.applicationContentsPath, "Mono/bin");
var value = pathEnv + ":" + monoPath;
var target = System.EnvironmentVariableTarget.Process;
System.Environment.SetEnvironmentVariable("PATH", value, target);
}
[PreferenceItem("Macro Killer")]
static void OnPreferencesGUI()
{
s_MacroCount = EditorPrefs.GetInt(s_MacroCountEditorKey, s_MacroCountDefaultValue);
EditorGUI.BeginChangeCheck();
int v = EditorGUILayout.IntSlider("Slots", s_MacroCount, s_MacroCountMinValue, s_MacroCountMaxValue);
if (EditorGUI.EndChangeCheck())
{
s_MacroCount = v;
EditorPrefs.SetInt(s_MacroCountEditorKey, v);
RepaintWindow();
}
if (GUILayout.Button("Clear Macros", EditorStyles.miniButton))
{
if (EditorUtility.DisplayDialog("Clear Macros",
"You are going to clear all the content. Are you sure?",
"Proceed",
"Cancel"))
{
for (int i = 0; i < s_MacroCountMaxValue; i++)
{
var k = string.Format(s_MacroKeyFormat, i);
if (EditorPrefs.HasKey(k))
{
EditorPrefs.DeleteKey(k);
}
}
}
RepaintWindow();
}
}
[MenuItem("Window/Macro Killer %#&m")]
public static void OpenWindow()
{
var dockNextToType = typeof(Editor).Assembly.GetType("UnityEditor.ConsoleWindow");
GetWindow<MacroKiller>("Macro Killer", dockNextToType);
}
public static void CloseWindow()
{
EditorWindow.FocusWindowIfItsOpen<MacroKiller>();
var w = EditorWindow.focusedWindow;
if (w.GetType() == typeof(MacroKiller))
{
w.Close();
}
}
public static void RepaintWindow()
{
EditorWindow.FocusWindowIfItsOpen<MacroKiller>();
var w = EditorWindow.focusedWindow;
if (w.GetType() == typeof(MacroKiller))
{
w.Repaint();
}
}
void OnFocus()
{
s_MacroCount = EditorPrefs.GetInt(s_MacroCountEditorKey, s_MacroCountDefaultValue);
}
void ModifierKeysChanged()
{
Repaint();
}
void Evaluate()
{
m_Error = false;
try
{
m_Output = Compile(m_Text);
}
catch (System.Exception e)
{
m_Error = true;
m_Output = e.Message;
}
finally
{
EditorGUIUtility.editingTextField = false;
}
}
bool HasInjection(CompilerResults results, out string ret)
{
ret = string.Empty;
var evaluatorType = results.CompiledAssembly.GetType("__macrokiller.Evaluator");
var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
if (evaluatorType.GetMembers(flags)
.Where(x => x.DeclaringType == evaluatorType)
.Where(y => y.GetCustomAttributes(typeof(CompilerGeneratedAttribute), true).Length == 0).Count() > 2)
{
ret = "You shall not pass.";
return true;
}
return false;
}
bool GetErrors(string methodBody, CompilerResults results, out string ret)
{
ret = string.Empty;
var errors = results.Errors;
if (errors.Count == 0)
{
return HasInjection(results, out ret);
}
foreach (var e in errors)
{
Match m = Regex.Match(e.ToString(), s_CompilerErrorPattern);
if (m.Success)
{
var file = m.Groups[1].Value;
var line = int.Parse(m.Groups[2].Value) - 10;
var column = int.Parse(m.Groups[3].Value);
var fullError = m.Groups[4].Value;
var type = m.Groups[5].Value;
var code = m.Groups[6].Value;
var message = m.Groups[7].Value;
if (line < 0 || line > methodBody.Split('\n').Length)
{
ret = string.Format("{0} out of bounds ({1})", type, code);
continue;
}
if (code == "CS0127")
{
message = "Do not use 'return <value>;'. Use 'output = <value>;' instead.";
}
ret += string.Format("{0} on {1},{2} ({3}): {4}", type, line, column, code, message);
}
else
{
ret += e.ToString();
}
ret += "\n";
}
return true;
}
string Compile(string methodBody)
{
var outputFieldName = "output";
if (!methodBody.Contains("\n") && !methodBody.EndsWith(";"))
{
methodBody += ";";
}
var source = string.Format(s_BaseSource, methodBody, outputFieldName);
var compilerParams = new CompilerParameters { GenerateInMemory = true, GenerateExecutable = false };
foreach (var asm in System.AppDomain.CurrentDomain.GetAssemblies())
{
if (!File.Exists(new System.Uri(asm.CodeBase).AbsolutePath))
{
continue;
}
compilerParams.ReferencedAssemblies.Add(asm.CodeBase);
}
lock (m_LockCompile)
{
using (var provider = new CSharpCodeProvider())
{
var results = provider.CompileAssemblyFromSource(compilerParams, source);
var errorString = string.Empty;
if (GetErrors(methodBody, results, out errorString))
{
throw new System.Exception(errorString);
}
var evaluatorType = results.CompiledAssembly.GetType("__macrokiller.Evaluator");
var methodInfo = evaluatorType.GetMethod("Eval");
methodInfo.Invoke(null, null);
var outputFieldInfo = evaluatorType.GetField(outputFieldName, BindingFlags.NonPublic | BindingFlags.Static);
var outputValue = outputFieldInfo.GetValue(null);
if (outputValue == null)
{
return "null";
}
else
{
return outputValue.ToString();
}
}
}
}
void Clear()
{
Undo.RecordObject(this, "Clear Macros");
m_Text = string.Empty;
m_Output = string.Empty;
m_Error = false;
EditorGUIUtility.editingTextField = false;
}
void ShowHelp()
{
Undo.RecordObject(this, "Show Help");
m_Text = "I'm the input field! I give you C# method scope, beware of the limitations! Don't try to fool me.\nClick Evaluate or CMD+Enter to get your code compiled and executed.\n\nAt the bottom you can find the Macro slots. You can store code snippets in every one of them.\nYou can change how many slots you want from Preferences.\nALT+Click: Save/overwrite macro\nCTRL+Click: Delete macro\nClick: Load macro";
m_Output = "I'm the output field! I print the string value of the 'output' variable you enter in the input field.\nI will print compiler errors too, in case something goes wrong.";
m_Error = false;
EditorGUIUtility.editingTextField = false;
}
void OnGUI()
{
if (Event.current.command)
{
if (Event.current.type == EventType.KeyDown)
{
if (Event.current.keyCode == KeyCode.Return)
{
Evaluate();
Event.current.Use();
}
if (Event.current.keyCode == KeyCode.Backspace)
{
Clear();
Event.current.Use();
}
}
}
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)))
{
Clear();
}
GUILayout.Space(6f);
if (GUILayout.Button("Evaluate", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)))
{
Evaluate();
}
GUIStyle versionLabel = new GUIStyle(EditorStyles.miniLabel);
versionLabel.alignment = TextAnchor.MiddleRight;
GUIContent rest = new GUIContent("Macro Killer");
Rect rect = GUILayoutUtility.GetRect(rest, EditorStyles.toolbar, GUILayout.ExpandWidth(true));
GUI.Label(rect, rest, versionLabel);
GUILayout.Space(4f);
if (GUILayout.Button("?", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)))
{
ShowHelp();
}
EditorGUILayout.EndHorizontal();
Color bg = GUI.backgroundColor;
if (m_Error)
{
GUI.backgroundColor = Color.red;
}
EditorGUI.BeginChangeCheck();
m_Text = EditorGUILayout.TextArea(m_Text, GUILayout.ExpandHeight(true));
if (EditorGUI.EndChangeCheck())
{
m_Error = false;
}
EditorGUILayout.LabelField("Output");
EditorGUILayout.SelectableLabel(m_Output, EditorStyles.textField, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(false));
GUI.backgroundColor = bg;
GUILayout.Space(3f);
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
for (int i = 1; i <= s_MacroCount; i++)
{
string macroKey = string.Format(s_MacroKeyFormat, i);
bool hasMacro = EditorPrefs.HasKey(macroKey);
string macroText = string.Empty;
if (hasMacro)
{
macroText = EditorPrefs.GetString(macroKey);
GUI.backgroundColor = Color.cyan;
}
var text = string.Format(s_MacroTitleKeyFormat, i);
if (hasMacro && Event.current.control)
{
GUI.backgroundColor = Color.red;
if (GUILayout.Button(text, EditorStyles.toolbarButton))
{
EditorGUIUtility.editingTextField = false;
if (EditorUtility.DisplayDialog("Delete Macro",
"You are going to delete " + text + ". Are you sure?",
"Delete",
"Cancel"))
{
EditorPrefs.DeleteKey(macroKey);
}
}
GUI.backgroundColor = bg;
}
else if (Event.current.alt)
{
GUI.backgroundColor = Color.green;
if (GUILayout.Button(text, EditorStyles.toolbarButton))
{
EditorGUIUtility.editingTextField = false;
if (!hasMacro || (hasMacro && EditorUtility.DisplayDialog("Overwrite Macro",
"You are going to overwrite " + text + ". Are you sure?",
"Overwrite",
"Cancel")))
{
EditorPrefs.SetString(macroKey, m_Text);
}
}
GUI.backgroundColor = bg;
}
else
{
if (GUILayout.Button(text, EditorStyles.toolbarButton))
{
EditorGUIUtility.editingTextField = false;
if (hasMacro)
{
m_Text = macroText;
}
else
{
ShowNotification(new GUIContent(text + " is empty."));
}
}
}
GUI.backgroundColor = bg;
}
EditorGUILayout.EndHorizontal();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment