├── .gitignore ├── Evaluator.cs ├── Evaluator.cs.meta ├── LICENSE ├── LICENSE.meta ├── Plugins.meta ├── Plugins ├── Mono.CSharp.dll └── Mono.CSharp.dll.meta ├── PrettyPrint.cs ├── PrettyPrint.cs.meta ├── README.textile ├── README.textile.meta ├── ReflectionProxy.cs ├── ReflectionProxy.cs.meta ├── Shell.cs ├── Shell.cs.meta ├── UnityREPLHelper.cs └── UnityREPLHelper.cs.meta /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Evaluator.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------- 2 | // Core evaluation loop, including environment handling for living in the Unity 3 | // editor and dealing with its code reloading behaviors. 4 | //----------------------------------------------------------------- 5 | using UnityEngine; 6 | using UnityEditor; 7 | using System; 8 | using System.Collections; 9 | using System.Collections.Generic; 10 | using System.Reflection; 11 | using System.Text; 12 | using System.Threading; 13 | using System.IO; 14 | using Mono.CSharp; 15 | 16 | class EvaluationException : Exception { 17 | } 18 | 19 | class EvaluationHelper { 20 | public EvaluationHelper() { 21 | TryLoadingAssemblies(false); 22 | } 23 | 24 | protected bool TryLoadingAssemblies(bool isInitialized) { 25 | if(isInitialized) 26 | return true; 27 | 28 | // Debug.Log("Attempting to load assemblies..."); 29 | 30 | foreach(Assembly b in AppDomain.CurrentDomain.GetAssemblies()) { 31 | string assemblyShortName = b.GetName().Name; 32 | if(!(assemblyShortName.StartsWith("Mono.CSharp") || assemblyShortName.StartsWith("UnityDomainLoad") || 33 | assemblyShortName.StartsWith("interactive"))) { 34 | //Debug.Log("Giving Mono.CSharp a reference to assembly: " + assemblyShortName); 35 | Evaluator.ReferenceAssembly(b); 36 | } 37 | } 38 | 39 | 40 | // These won't work the first time through after an assembly reload. No 41 | // clue why, but the Unity* namespaces don't get found. Perhaps they're 42 | // being loaded into our AppDomain asynchronously and just aren't done yet? 43 | // Regardless, attempting to hit them early and then trying again later 44 | // seems to work fine. 45 | Evaluator.Run("using System;"); 46 | Evaluator.Run("using System.IO;"); 47 | Evaluator.Run("using System.Linq;"); 48 | Evaluator.Run("using System.Collections;"); 49 | Evaluator.Run("using System.Collections.Generic;"); 50 | Evaluator.Run("using UnityEditor;"); 51 | Evaluator.Run("using UnityEngine;"); 52 | 53 | return true; 54 | } 55 | 56 | public void Init(ref bool isInitialized) { 57 | /* 58 | We need to tell the evaluator to reference stuff we care about. Since 59 | there's a lot of dynamically named stuff that we might want, we just pull 60 | the list of loaded assemblies and include them "all" (with the exception of 61 | a couple that I have a sneaking suspicion may be bad to reference -- noted 62 | below). 63 | 64 | Examples of what we might get when asking the current AppDomain for all 65 | assemblies (short names only): 66 | 67 | Stuff we avoid: 68 | UnityDomainLoad <-- Unity gubbins. Probably want to avoid this. 69 | Mono.CSharp <-- The self-same package used to pull this off. Probably 70 | safe, but not taking any chances. 71 | interactive0 <-- Looks like what Mono.CSharp is making on the fly. If we 72 | load those, it APPEARS we may wind up holding onto them 73 | 'forever', so... Don't even try. 74 | 75 | 76 | Mono runtime, which we probably get 'for free', but include just in case: 77 | System 78 | mscorlib 79 | 80 | Unity runtime, which we definitely want: 81 | UnityEditor 82 | UnityEngine 83 | UnityScript.Lang 84 | Boo.Lang 85 | 86 | The assemblies Unity generated from our project code now all begin with Assembly: 87 | Assembly-CSharp 88 | Assembly-CSharp-Editor 89 | ... 90 | */ 91 | isInitialized = TryLoadingAssemblies(isInitialized); 92 | 93 | if(Evaluator.InteractiveBaseClass != typeof(UnityBaseClass)) 94 | Evaluator.InteractiveBaseClass = typeof(UnityBaseClass); 95 | } 96 | 97 | public bool Eval(string code) { 98 | EditorApplication.LockReloadAssemblies(); 99 | 100 | bool status = false, 101 | hasOutput = false; 102 | object output = null; 103 | string res = null, 104 | tmpCode = code.Trim(); 105 | // Debug.Log("Evaluating: " + tmpCode); 106 | 107 | try { 108 | if(tmpCode.StartsWith("=")) { 109 | // Special case handling of calculator mode. The problem is that 110 | // expressions involving multiplication are grammatically ambiguous 111 | // without a var declaration or some other grammatical construct. 112 | // TODO: Change the prompt in calculator mode. Needs to be done from Shell. 113 | tmpCode = "(" + tmpCode.Substring(1, tmpCode.Length - 1) + ");"; 114 | } 115 | res = Evaluate(tmpCode, out output, out hasOutput); 116 | } catch(EvaluationException) { 117 | Debug.LogError(@"Error compiling/executing code. Please double-check syntax, method/variable names, etc. 118 | You can find more information in Unity's `Editor.log` file (*not* the editor console!)."); 119 | 120 | output = new Evaluator.NoValueSet(); 121 | hasOutput = false; 122 | res = tmpCode; // Enable continued editing on syntax errors, etc. 123 | } catch(Exception e) { 124 | Debug.LogError(e); 125 | 126 | res = tmpCode; // Enable continued editing on unexpected errors. 127 | } finally { 128 | status = res == null; 129 | } 130 | 131 | if(hasOutput) { 132 | if(status) { 133 | try { 134 | StringBuilder sb = new StringBuilder(); 135 | PrettyPrint.PP(sb, output, true); 136 | Debug.Log(sb.ToString()); 137 | } catch(Exception e) { 138 | Debug.LogError(e.ToString().Trim()); 139 | } 140 | } 141 | } 142 | 143 | EditorApplication.UnlockReloadAssemblies(); 144 | return status; 145 | } 146 | 147 | /* Copy-pasta'd from the DLL to try and differentiate between kinds of failure mode. */ 148 | private string Evaluate(string input, out object result, out bool result_set) { 149 | result_set = false; 150 | result = null; 151 | 152 | CompiledMethod compiledMethod; 153 | string remainder = null; 154 | remainder = Evaluator.Compile(input, out compiledMethod); 155 | if(remainder != null) 156 | return remainder; 157 | if(compiledMethod == null) 158 | throw new EvaluationException(); 159 | 160 | object typeFromHandle = typeof(Evaluator.NoValueSet); 161 | try { 162 | EvaluatorProxy.invoke_thread = Thread.CurrentThread; 163 | EvaluatorProxy.invoking = true; 164 | compiledMethod(ref typeFromHandle); 165 | } catch(ThreadAbortException arg) { 166 | Thread.ResetAbort(); 167 | Console.WriteLine("Interrupted!\n{0}", arg); 168 | // TODO: How best to handle this? 169 | } finally { 170 | EvaluatorProxy.invoking = false; 171 | } 172 | if(typeFromHandle != typeof(Evaluator.NoValueSet)) { 173 | result_set = true; 174 | result = typeFromHandle; 175 | } 176 | return null; 177 | } 178 | } 179 | 180 | // WARNING: Absolutely NOT thread-safe! 181 | internal class EvaluatorProxy : ReflectionProxy { 182 | private static readonly Type _Evaluator = typeof(Evaluator); 183 | private static readonly FieldInfo _fields = _Evaluator.GetField("fields", NONPUBLIC_STATIC); 184 | private static readonly FieldInfo _invoke_thread = _Evaluator.GetField("invoke_thread", NONPUBLIC_STATIC); 185 | private static readonly FieldInfo _invoking = _Evaluator.GetField("invoking", NONPUBLIC_STATIC); 186 | 187 | internal static Hashtable fields { get { return (Hashtable)_fields.GetValue(null); } } 188 | internal static Thread invoke_thread { 189 | get { return (Thread)_invoke_thread.GetValue(null); } 190 | set { _invoke_thread.SetValue(null, value); } 191 | } 192 | internal static bool invoking { 193 | get { return (bool)_invoking.GetValue(false); } 194 | set { _invoking.SetValue(null, value); } 195 | } 196 | } 197 | 198 | // WARNING: Absolutely NOT thread-safe! 199 | internal class TypeManagerProxy : ReflectionProxy { 200 | private static readonly Type _TypeManager = typeof(Evaluator).Assembly.GetType("Mono.CSharp.TypeManager"); 201 | private static readonly MethodInfo _CSharpName = _TypeManager.GetMethod("CSharpName", 202 | PUBLIC_STATIC, 203 | null, 204 | Signature(typeof(Type)), 205 | null); 206 | 207 | // Save an allocation per access here... 208 | private static readonly object[] _CSharpNameParams = new object[] { null }; 209 | 210 | internal static string CSharpName(Type t) { 211 | // TODO: What am I doing wrong here that this throws on generics?? 212 | string name = ""; 213 | try { 214 | _CSharpNameParams[0] = t; 215 | name = (string)_CSharpName.Invoke(null, _CSharpNameParams); 216 | } catch(Exception) { 217 | name = "?"; 218 | } 219 | return name; 220 | } 221 | } 222 | 223 | // Dummy class so we can output a string and bypass pretty-printing of it. 224 | public struct REPLMessage { 225 | public string msg; 226 | 227 | public REPLMessage(string m) { 228 | msg = m; 229 | } 230 | } 231 | 232 | public class UnityBaseClass { 233 | private static readonly REPLMessage _help = new REPLMessage(@"UnityREPL v." + Shell.VERSION + @": 234 | 235 | help; -- This screen; help for helper commands. Click the '?' icon on the toolbar for more comprehensive help. 236 | vars; -- Show the variables you've created this session, and their current values. 237 | "); 238 | 239 | public static REPLMessage help { get { return _help; } } 240 | 241 | public static REPLMessage vars { 242 | get { 243 | Hashtable fields = EvaluatorProxy.fields; 244 | StringBuilder tmp = new StringBuilder(); 245 | // TODO: Sort this list... 246 | foreach(DictionaryEntry kvp in fields) { 247 | FieldInfo field = (FieldInfo)kvp.Value; 248 | tmp 249 | .Append(TypeManagerProxy.CSharpName(field.FieldType)) 250 | .Append(" ") 251 | .Append(kvp.Key) 252 | .Append(" = "); 253 | PrettyPrint.PP(tmp, field.GetValue(null)); 254 | tmp.Append(";\n"); 255 | } 256 | return new REPLMessage(tmp.ToString()); 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Evaluator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ac314cd125b0c41b6927d1b43c2784b2 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010-2016 Jon Frisby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 628c13909fdcc40d1883caaa9e9811bf 3 | DefaultImporter: 4 | userData: 5 | -------------------------------------------------------------------------------- /Plugins.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65aaa4bbe089e43429f7930389814a8a 3 | -------------------------------------------------------------------------------- /Plugins/Mono.CSharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrJoy/UnityREPL/435909ae7b128dfa6bfd7c5d47e2014d1fa7345a/Plugins/Mono.CSharp.dll -------------------------------------------------------------------------------- /Plugins/Mono.CSharp.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b7ca75ef29fd44614bb5e6c3783f10c6 3 | PluginImporter: 4 | serializedVersion: 1 5 | iconMap: {} 6 | executionOrder: {} 7 | isPreloaded: 0 8 | platformData: 9 | Any: 10 | enabled: 0 11 | settings: {} 12 | Editor: 13 | enabled: 1 14 | settings: 15 | DefaultValueInitialized: true 16 | WindowsStoreApps: 17 | enabled: 0 18 | settings: 19 | CPU: AnyCPU 20 | userData: 21 | assetBundleName: 22 | assetBundleVariant: 23 | -------------------------------------------------------------------------------- /PrettyPrint.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------- 2 | // Adapted from repl.cs: 3 | // https://github.com/mono/mono/tree/master/mcs/tools/csharp 4 | // 5 | //----------------------------------------------------------------- 6 | using System; 7 | using System.Collections; 8 | using System.Text; 9 | using Mono.CSharp; 10 | using UnityEngine; 11 | 12 | public class PrettyPrint { 13 | static string EscapeString(string s) { 14 | return s.Replace("\"", "\\\""); 15 | } 16 | 17 | static void EscapeChar(StringBuilder output, char c) { 18 | if(c == '\'') 19 | output.Append("'\\''"); 20 | else if(c > 32) 21 | output.AppendFormat("'{0}'", c); 22 | else { 23 | switch(c) { 24 | case '\a': 25 | output.Append("'\\a'"); 26 | break; 27 | case '\b': 28 | output.Append("'\\b'"); 29 | break; 30 | case '\n': 31 | output.Append("'\\n'"); 32 | break; 33 | case '\v': 34 | output.Append("'\\v'"); 35 | break; 36 | case '\r': 37 | output.Append("'\\r'"); 38 | break; 39 | case '\f': 40 | output.Append("'\\f'"); 41 | break; 42 | case '\t': 43 | output.Append("'\\t"); 44 | break; 45 | default: 46 | output.AppendFormat("'\\x{0:x}", (int)c); 47 | break; 48 | } 49 | } 50 | } 51 | 52 | private static int _depth = 0; 53 | 54 | private static void OpenInline(StringBuilder output, int listLength) { 55 | output.Append(listLength < 10 ? "{ " : "{\n\t"); 56 | _depth++; 57 | } 58 | 59 | private static void CloseInline(StringBuilder output, int listLength) { 60 | output.Append(listLength < 10 ? " }" : "\n}"); 61 | _depth--; 62 | } 63 | 64 | private static void NextItem(StringBuilder output, int listLength) { 65 | output.Append(listLength < 10 ? ", " : ",\n\t"); 66 | } 67 | 68 | private static void Open(StringBuilder output) { 69 | output.Append("{"); 70 | _depth++; 71 | } 72 | 73 | private static void Close(StringBuilder output) { 74 | output.Append("}"); 75 | _depth--; 76 | } 77 | 78 | public static void PP(StringBuilder output, object result, bool expandTypes = false) { 79 | _depth = 0; 80 | InternalPP(output, result, expandTypes); 81 | } 82 | 83 | protected static void InternalPP(StringBuilder output, object result, bool expandTypes = false) { 84 | if(result == null) 85 | output.Append("null"); 86 | else { 87 | if(result is REPLMessage) { 88 | // Raw, no escaping or quoting. 89 | output.Append(((REPLMessage)result).msg); 90 | } else if(result is Component) { 91 | string n; 92 | try { 93 | n = ((Component)result).name; 94 | } catch(MissingReferenceException) { 95 | n = ""; 96 | } 97 | output.Append(n); 98 | } else if(result is GameObject) { 99 | string n; 100 | try { 101 | n = ((GameObject)result).name; 102 | } catch(MissingReferenceException) { 103 | n = ""; 104 | } 105 | output.Append(n); 106 | } else if(result is Array) { 107 | Array a = (Array)result; 108 | int top = a.GetUpperBound(0), bottom = a.GetLowerBound(0); 109 | OpenInline(output, top - bottom); 110 | for(int i = bottom; i <= top; i++) { 111 | InternalPP(output, a.GetValue(i)); 112 | if(i != top) 113 | NextItem(output, top - bottom); 114 | } 115 | CloseInline(output, top - bottom); 116 | } else if(result is bool) 117 | output.Append(((bool)result) ? "true" : "false"); 118 | else if(result is string) 119 | output.Append('"').Append(EscapeString((string)result)).Append('"'); 120 | else if(result is IDictionary) { 121 | IDictionary dict = (IDictionary)result; 122 | int top = dict.Count, count = 0; 123 | Open(output); 124 | foreach(DictionaryEntry entry in dict) { 125 | count++; 126 | InternalPP(output, entry.Key); 127 | output.Append(": "); 128 | InternalPP(output, entry.Value); 129 | if(count != top) 130 | NextItem(output, 0); 131 | } 132 | Close(output); 133 | } else if(result is IEnumerable) { 134 | int i = 0; 135 | ArrayList tmp = new ArrayList(); 136 | foreach(object item in(IEnumerable)result) 137 | tmp.Add(item); 138 | OpenInline(output, tmp.Count); 139 | foreach(object item in tmp) { 140 | if(i++ != 0) 141 | NextItem(output, tmp.Count); 142 | InternalPP(output, item); 143 | } 144 | CloseInline(output, tmp.Count); 145 | } else if(result is char) 146 | EscapeChar(output, (char)result); 147 | else if(result is Type || result.GetType().Name == "MonoType") { 148 | if(_depth > 0 || !expandTypes) 149 | output.Append("typeof(" + ((Type)result).Namespace + "." + ((Type)result).Name + ")"); 150 | else 151 | output.Append(InteractiveBase.Describe(result)); 152 | } else 153 | output.Append(result.ToString()); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /PrettyPrint.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9ea89251aa90441e2b16fc324076be78 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. OVERVIEW 2 | 3 | UnityREPL is a REPL (Read, Eval, Print, Loop) tool based on "Miguel's CSharpRepl":CSharpRepl but for "Unity":Unity. 4 | 5 | What's a REPL? See the "Wikipedia article.":Wikipedia or the "demo videos":Wiki on the Wiki. 6 | 7 | 8 | h1. INSTALLATION 9 | 10 | Please note that this requires UnityGUIExtensions. The .unitypackage includes everything you need 11 | 12 | h2. NON-GIT, OR UNITY-INDIE SETUP 13 | 14 | * Grab the ".unitypackage":Package from the downloads page. 15 | * Open your project. 16 | * Click "Assets" -> "Import Package..." from the menu, and select the file. 17 | 18 | 19 | h2. GIT SETUP IN A UNITY-PRO PROJECT THAT IS NOT VERSIONED WITH GIT 20 | 21 | *You must have External Version Control enabled to use these instructions. If not, please download the .unitypackage and use that.* 22 | 23 | bc. cd myproject/ 24 | mkdir -p Assets/Editor/ 25 | git clone git://github.com/MrJoy/UnityREPL.git Assets/Editor/UnityREPL 26 | git clone git://github.com/MrJoy/UnityGUIExtensions.git Assets/UnityGUIExtensions 27 | 28 | By setting up this way, you can track updates using "git pull". 29 | 30 | 31 | h2. GIT SETUP IN A UNITY-PRO PROJECT VERSIONED WITH GIT 32 | 33 | *You must have External Version Control enabled to use these instructions. If not, please download the .unitypackage and use that.* 34 | 35 | bc. cd myproject/ 36 | mkdir -p Assets/Editor 37 | git submodule add git://github.com/MrJoy/UnityREPL.git Assets/Editor/UnityREPL 38 | git submodule add git://github.com/MrJoy/UnityGUIExtensions.git Assets/UnityGUIExtensions 39 | git submodule init 40 | git submodule update 41 | 42 | 43 | h1. USAGE 44 | 45 | * Click "Window" -> "C# Shell" 46 | * Dock the new tab wherever you wish, and enjoy! 47 | 48 | From this window you can enter C# code and run it. You should have access to all classes you've defined in C#, JavaScript, or Boo in your project. 49 | 50 | 51 | h1. COMPATIBILITY 52 | 53 | Tested on Unity 5.3.1, but should work on slightly earlier versions down to, perhaps 5.0. 54 | 55 | Will definitely not work on older versions of Unity (4.x and below). 56 | 57 | 58 | h1. DOCUMENTATION 59 | 60 | Full documentation is available from within UnityREPL by clicking the '?' button on the toolbar. 61 | 62 | 63 | h1. LICENSE 64 | 65 | All original/novel code is Copyright (c) 2009-2016 Jon Frisby. All other code is the property of the respective authors. 66 | 67 | Dual licensed under the terms of the MIT X11 or GNU GPL, as per the original code. 68 | 69 | The included Mono.CSharp.dll is a completely unmodified copy from "Mono 2.4.":Mono, please see the "Mono Project":Mono for details on obtaining source to it. 70 | 71 | 72 | h1. TODO 73 | 74 | * Undo functionality. 75 | * Editing history support (Bash style). 76 | * Smooth out some hiccups with assembly reloads. 77 | ** Local vars getting blown away. 78 | ** Make sure we're not leaking references to things. 79 | * Add things to UnityBaseClass to help with Unity workflows. 80 | * Lots of other little things; see the source code for more info. 81 | 82 | 83 | [Mono]http://www.mono-project.com 84 | [Wiki]http://wiki.github.com/MrJoy/UnityREPL/ 85 | [Wikipedia]http://en.wikipedia.org/wiki/Read_Eval_Print_Loop 86 | [CSharpRepl]http://www.mono-project.com/CsharpRepl 87 | [Unity]http://Unity3D.com 88 | [Package]https://github.com/MrJoy/UnityREPL/releases/download/releases%2F2.0.1/UnityREPL_2.0.1.unitypackage 89 | -------------------------------------------------------------------------------- /README.textile.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2d6a01b53ac5143d4ac5483873f36350 3 | DefaultImporter: 4 | userData: 5 | -------------------------------------------------------------------------------- /ReflectionProxy.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------- 2 | // Base class for utility classes to make reflection less cumbersome to use. 3 | //----------------------------------------------------------------- 4 | using System; 5 | using System.Reflection; 6 | 7 | internal class ReflectionProxy { 8 | internal const BindingFlags PUBLIC_STATIC = BindingFlags.Public | BindingFlags.Static; 9 | internal const BindingFlags NONPUBLIC_STATIC = BindingFlags.NonPublic | BindingFlags.Static; 10 | 11 | // Turn one or more `Type`s into an array. 12 | protected static Type[] Signature(params Type[] sig) { 13 | return sig; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ReflectionProxy.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fac94124a31954a03bd8c1e0e7dad454 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /Shell.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------- 2 | // Main Unity shell for REPL. 3 | // 4 | // TODO: Make sure this plays nice with assembly reloads by: 5 | // A) Make sure the newest version of our code is made available after a 6 | // reload. 7 | // B) Make sure we're not inadvertently 'leaking' things into Mono's heap 8 | // (I.E. preventing unloading of old stuff) via dangling references and 9 | // such. 10 | // TODO: Integrate with Mono.CSharp.Report to get warning/error info in a more 11 | // elegant manner (just capturing it all raw is bad since we can't 12 | // reliably format it.) 13 | // TODO: Format Unity objects more gracefully. 14 | // TODO: Turn editor components into more general, reusable GUI widgets. 15 | // TODO: Suss out undo and wrap code execution accordingly. 16 | // TODO: Suss out undo and wrap editor accordingly. 17 | // TODO: Make use of `EditorWindow.minSize`/`EditorWindow.maxSize`. 18 | // TODO: Look at `EditorGUI.FocusTextInControl`. 19 | // TODO: See about organizing and making code more bulletproof with `HorizontalScope` / `VerticalScope` / `ScrollViewScope`. 20 | // TODO: See `EditorJsonUtility` for managing history explicitly instead of relying on editor. 21 | // TODO: See `PopupWindow` for consolidating options on the toolbar, as we add more. 22 | //----------------------------------------------------------------- 23 | #if UNITY_5_2 || UNITY_5_1 || UNITY_5_0 || UNITY_4_6 || UNITY_4_5 || UNITY_4_4 || UNITY_4_3 || UNITY_4_2 || UNITY_4_1 || UNITY_4_0_1 || UNITY_4_0 24 | #define UNITY_5_3_PLUS 25 | #endif 26 | 27 | using UnityEditor; 28 | using UnityEngine; 29 | using System; 30 | using System.Collections; 31 | using System.Collections.Generic; 32 | using System.Reflection; 33 | using System.Text; 34 | 35 | // TODO: 36 | // static string[] Evaluator.GetCompletions(string input, out string prefix) <----- 37 | 38 | public class Shell : EditorWindow { 39 | //---------------------------------------------------------------------------- 40 | // Constants, specified here to keep things DRY. 41 | //---------------------------------------------------------------------------- 42 | public const string VERSION = "2.0.1", 43 | COPYRIGHT = "(C) Copyright 2009-2015 Jon Frisby\nAll rights reserved", 44 | 45 | MAIN_PROMPT = "---->", 46 | CONTINUATION_PROMPT = "cont>"; 47 | 48 | //---------------------------------------------------------------------------- 49 | // Code Execution Functionality 50 | //---------------------------------------------------------------------------- 51 | private EvaluationHelper helper = new EvaluationHelper(); 52 | [System.NonSerialized] 53 | private bool isInitialized = false; 54 | 55 | public void Update() { 56 | if(doProcess) { 57 | // Don't be executing code when we're about to reload it. Not sure this is 58 | // actually needed but seems prudent to be wary of it. 59 | if(EditorApplication.isCompiling) 60 | return; 61 | 62 | helper.Init(ref isInitialized); 63 | doProcess = false; 64 | bool compiledCorrectly = helper.Eval(codeToProcess); 65 | if(compiledCorrectly) 66 | resetCommand = true; 67 | else { 68 | // Continue with what enter the user pressed... Yes, this is an ugly 69 | // way to handle it. 70 | codeToProcess = Paste(editorState, "\n", false); 71 | } 72 | } 73 | } 74 | //---------------------------------------------------------------------------- 75 | 76 | 77 | //---------------------------------------------------------------------------- 78 | // Code Editor Functionality 79 | //---------------------------------------------------------------------------- 80 | private bool doProcess = false, 81 | useContinuationPrompt = false, 82 | resetCommand = false; 83 | private string codeToProcess = ""; 84 | 85 | // WARNING: Undocumented spookiness from deep within the bowels of Unity! 86 | public TextEditor editorState = null; 87 | 88 | // Need to use menu items because otherwise we don't receive events for cmd-] 89 | // and cmd-[. 90 | [MenuItem("Edit/Indent %]", false, 256)] 91 | public static void IndentCommand() { 92 | Shell w = focusedWindow as Shell; 93 | if(w != null) { 94 | w.codeToProcess = w.Indent(w.editorState); 95 | w.Repaint(); 96 | } 97 | } 98 | 99 | [MenuItem("Edit/Unindent %[", false, 256)] 100 | public static void UnindentCommand() { 101 | Shell w = focusedWindow as Shell; 102 | if(w != null) { 103 | w.codeToProcess = w.Unindent(w.editorState); 104 | w.Repaint(); 105 | } 106 | } 107 | 108 | [MenuItem("Edit/Indent %]", true)] 109 | public static bool ValidateIndentCommand() { 110 | Shell w = focusedWindow as Shell; 111 | return (w != null) && (w.editorState != null); 112 | } 113 | 114 | [MenuItem("Edit/Unindent %[", true)] 115 | public static bool ValidateUnindentCommand() { 116 | Shell w = focusedWindow as Shell; 117 | return (w != null) && (w.editorState != null); 118 | } 119 | 120 | // Make our state object go away if we do, or if we lose focus, or whatnot 121 | // to ensure menu items disable properly regardless of possible dangling 122 | // references, etc. 123 | public void OnDisable() { editorState = null; } 124 | public void OnLostFocus() { editorState = null; } 125 | public void OnDestroy() { editorState = null; } 126 | 127 | public string Indent(TextEditor editor) { 128 | if(editor.hasSelection) { 129 | string codeToIndent = editor.SelectedText; 130 | string[] rawLines = codeToIndent.Split('\n'); 131 | for(int i = 0; i < rawLines.Length; i++) 132 | rawLines[i] = '\t' + rawLines[i]; 133 | 134 | // Eep! We don't want to indent a trailing empty line because that means 135 | // the user had a 'perfect' block selection and we're accidentally 136 | // indenting the next line. Yuck! 137 | if(rawLines[rawLines.Length - 1] == "\t") 138 | rawLines[rawLines.Length - 1] = ""; 139 | 140 | return Paste(editor, String.Join("\n", rawLines), true); 141 | } else { 142 | string[] rawLines = codeToProcess.Split('\n'); 143 | int counter = -1, curLine = 0; 144 | while((counter < editor.cursorIndex) && (curLine < rawLines.Length)) 145 | counter += rawLines[curLine++].Length + 1; // The +1 is for the \n. 146 | 147 | if(counter >= editor.cursorIndex) { 148 | curLine--; 149 | rawLines[curLine] = '\t' + rawLines[curLine]; 150 | editor.cursorIndex++; 151 | editor.selectIndex++; 152 | codeToProcess = String.Join("\n", rawLines); 153 | } 154 | 155 | return codeToProcess; 156 | } 157 | } 158 | 159 | public string Unindent(TextEditor editor) { 160 | if(editor.hasSelection) { 161 | string codeToIndent = editor.SelectedText; 162 | string[] rawLines = codeToIndent.Split('\n'); 163 | for(int i = 0; i < rawLines.Length; i++) { 164 | if(rawLines[i].StartsWith("\t")) 165 | rawLines[i] = rawLines[i].Substring(1); 166 | } 167 | return Paste(editor, String.Join("\n", rawLines), true); 168 | } else { 169 | string[] rawLines = codeToProcess.Split('\n'); 170 | int counter = 0, curLine = 0; 171 | while((counter < editor.cursorIndex) && (curLine < rawLines.Length)) 172 | counter += rawLines[curLine++].Length + 1; // The +1 is for the \n. 173 | 174 | if(counter >= editor.cursorIndex) { 175 | // If counter == editor.pos, then the cursor is at the beginning of a 176 | // line and we run into a couple annoying issues where the logic here 177 | // acts as though it should be operating on the previous line (OY!). 178 | // SO. To that end, we treat that as a bit of a special-case. We 179 | // don't decrement the current line counter if cursor is at start of 180 | // line: 181 | if(counter > editor.cursorIndex) 182 | curLine--; 183 | if(rawLines[curLine].StartsWith("\t")) { 184 | rawLines[curLine] = rawLines[curLine].Substring(1); 185 | // AAAAAAAAAND, we don't try to unindent the cursor. Although, truth 186 | // be told, we should probably preserve the TextMate-esaue edit 187 | // behavior of having the cursor not move when changing indentation. 188 | if(counter > editor.cursorIndex) { 189 | editor.cursorIndex--; 190 | editor.selectIndex--; 191 | } 192 | codeToProcess = String.Join("\n", rawLines); 193 | } 194 | } 195 | 196 | return codeToProcess; 197 | } 198 | } 199 | 200 | public string Paste(TextEditor editor, string textToPaste, bool continueSelection) { 201 | // The user can select from right-to-left and Unity gives us data that's 202 | // different than if they selected left-to-right. That can be handy, but 203 | // here we just want to know substring indexes to slice out. 204 | int startAt = Mathf.Min(editor.cursorIndex, editor.selectIndex), 205 | endAt = Mathf.Max(editor.cursorIndex, editor.selectIndex); 206 | string prefix = "", 207 | suffix = ""; 208 | #if UNITY_5_3_PLUS 209 | if(startAt > 0) 210 | prefix = editor.content.text.Substring(0, startAt); 211 | if(endAt < editor.content.text.Length) 212 | suffix = editor.content.text.Substring(endAt); 213 | #else 214 | if(startAt > 0) 215 | prefix = editor.text.Substring(0, startAt); 216 | if(endAt < editor.text.Length) 217 | suffix = editor.text.Substring(endAt); 218 | #endif 219 | string newCorpus = prefix + textToPaste + suffix; 220 | 221 | #if UNITY_5_3_PLUS 222 | editor.content.text = newCorpus; 223 | #else 224 | editor.text = newCorpus; 225 | #endif 226 | if(continueSelection) { 227 | if(editor.cursorIndex > editor.selectIndex) 228 | editor.cursorIndex = prefix.Length + textToPaste.Length; 229 | else 230 | editor.selectIndex = prefix.Length + textToPaste.Length; 231 | } else 232 | editor.cursorIndex = editor.selectIndex = prefix.Length + textToPaste.Length; 233 | return newCorpus; 234 | } 235 | 236 | public string Cut(TextEditor editor) { 237 | EditorGUIUtility.systemCopyBuffer = editor.SelectedText; 238 | 239 | // The user can select from right-to-left and Unity gives us data that's 240 | // different than if they selected left-to-right. That can be handy, but 241 | // here we just want to know substring indexes to slice out. 242 | int startAt = Mathf.Min(editor.cursorIndex, editor.selectIndex), 243 | endAt = Mathf.Max(editor.cursorIndex, editor.selectIndex); 244 | string prefix = "", 245 | suffix = ""; 246 | 247 | #if UNITY_5_3_PLUS 248 | if(startAt > 0) 249 | prefix = editor.content.text.Substring(0, startAt); 250 | if(endAt < editor.content.text.Length) 251 | suffix = editor.content.text.Substring(endAt); 252 | #else 253 | if(startAt > 0) 254 | prefix = editor.text.Substring(0, startAt); 255 | if(endAt < editor.text.Length) 256 | suffix = editor.text.Substring(endAt); 257 | #endif 258 | string newCorpus = prefix + suffix; 259 | 260 | #if UNITY_5_3_PLUS 261 | editor.content.text = newCorpus; 262 | #else 263 | editor.text = newCorpus; 264 | #endif 265 | editor.cursorIndex = editor.selectIndex = prefix.Length; 266 | return newCorpus; 267 | } 268 | 269 | // Handy-dandy method to deal with keyboard inputs which we get as actual 270 | // events. Basically lets us deal with copy & paste, etc which GUI.TextArea 271 | // ordinarily does not support. 272 | private void FilterEditorInputs() { 273 | Event evt = Event.current; 274 | if(focusedWindow == this) { 275 | // Only attempt to grab this if our window has focus in order to make 276 | // indent/unindent menu items behave sanely. 277 | int editorId = GUIUtility.keyboardControl; 278 | try { 279 | editorState = GUIUtility.QueryStateObject(typeof(System.Object), editorId) as TextEditor; 280 | } catch(KeyNotFoundException) { 281 | // Ignoring because this seems to only mean that no such object was found. 282 | } 283 | if(editorState == null) 284 | return; 285 | } else 286 | return; 287 | 288 | if(doProcess) { 289 | // If we're waiting for a command to run, don't muck with the text! 290 | if(evt.isKey) 291 | evt.Use(); 292 | return; 293 | } 294 | 295 | if(evt.isKey) { 296 | if(evt.type == EventType.KeyDown) { 297 | // KeyDown gets the key press + repeating. We only care about a few 298 | // things... 299 | if(evt.functionKey) { 300 | // TODO: Make sure we don't have modifier keys pressed... 301 | 302 | // TODO: Proper edit-history support! 303 | if(evt.keyCode == KeyCode.UpArrow) { 304 | // TODO: If we're at the top of the input, move to the previous 305 | // TODO: history item. If the current item is the last history item, 306 | // TODO: update the history with changes? 307 | } else if(evt.keyCode == KeyCode.DownArrow) { 308 | // TODO: If we're at the bottom of the input, move to the previous 309 | // TODO: history item. If the current item is the last history item, 310 | // TODO: update the history with changes? 311 | } 312 | } else if(evt.keyCode == KeyCode.Return) { 313 | // TODO: Do we only want to do this only when the cursor is at the 314 | // TODO: end of the input? (Avoids unexpectedly putting newlines in 315 | // TODO: the middle of peoples' input...) 316 | if(Event.current.shift) 317 | codeToProcess = Paste(editorState, "\n", false); 318 | else 319 | doProcess = true; 320 | useContinuationPrompt = true; // In case we fail. 321 | } else if(evt.keyCode == KeyCode.Tab) { 322 | // Unity doesn't like using tab for actual editing. We're gonna 323 | // change that. So here we inject a tab, and later we'll deal with 324 | // focus issues. 325 | codeToProcess = Paste(editorState, "\t", false); 326 | } 327 | } 328 | } else if(evt.type == EventType.ValidateCommand) { 329 | switch(evt.commandName) { 330 | case "SelectAll": 331 | case "Paste": 332 | // Always allowed to muck with selection or paste stuff... 333 | evt.Use(); 334 | break; 335 | case "Copy": 336 | case "Cut": 337 | // ... but can only copy & cut when we have a selection. 338 | if(editorState.hasSelection) 339 | evt.Use(); 340 | break; 341 | default: 342 | // If we need to suss out other commands to support... 343 | // Debug.Log("Validate: " + evt.commandName); 344 | break; 345 | } 346 | } else if(evt.type == EventType.ExecuteCommand) { 347 | switch(evt.commandName) { 348 | // A couple TextEditor functions actually work, so use them... 349 | case "SelectAll": 350 | editorState.SelectAll(); 351 | break; 352 | case "Copy": 353 | editorState.Copy(); 354 | break; 355 | // But some don't: 356 | case "Paste": 357 | // Manually paste. Keeping Use() out of the Paste() method so we can 358 | // re-use the functionality elsewhere. 359 | codeToProcess = Paste(editorState, EditorGUIUtility.systemCopyBuffer, false); 360 | evt.Use(); 361 | break; 362 | case "Cut": 363 | // Ditto -- manual cut. 364 | codeToProcess = Cut(editorState); 365 | evt.Use(); 366 | break; 367 | } 368 | } 369 | } 370 | 371 | private void ForceFocus(string selectedControl, string desiredControl) { 372 | // Now here's how we deal with tabbing and hitting enter and whatnot. 373 | // Basically, if we're the current editor window we assume that we always 374 | // want the editor to have focus. BUT there's a gotcha! If we just blindly 375 | // plow through with this, we'll wind up interfering with copy/paste/etc. 376 | // It SEEMS to be the case that if we constrain mucking with the focus until 377 | // the repaint event that stuff Just Works. 378 | // 379 | // The only issue remaining after that is the actual selection -- mucking 380 | // with the focus will cause it to get reset. So we need to capture and 381 | // restore it. 382 | if(focusedWindow == this) { 383 | if((Event.current != null) && (Event.current.type == EventType.Repaint)) { 384 | if(selectedControl != desiredControl) { 385 | int p = 0, sp = 0; 386 | if(editorState != null) { 387 | p = editorState.cursorIndex; 388 | sp = editorState.selectIndex; 389 | } 390 | GUI.FocusControl(desiredControl); 391 | if(editorState != null) { 392 | editorState.cursorIndex = p; 393 | editorState.selectIndex = sp; 394 | } 395 | } 396 | } 397 | } 398 | } 399 | 400 | private void HandleInputFocusAndStateForEditor() { 401 | string selectedControl = GUI.GetNameOfFocusedControl(); 402 | ForceFocus(selectedControl, editorControlName); 403 | if(selectedControl == editorControlName) 404 | FilterEditorInputs(); 405 | if(resetCommand) { 406 | resetCommand = false; 407 | useContinuationPrompt = false; 408 | codeToProcess = ""; 409 | } 410 | } 411 | 412 | private NumberedEditorState lnEditorState = new NumberedEditorState(); 413 | 414 | private void ShowEditor() { 415 | GUILayout.BeginHorizontal(); 416 | GUILayout.Label(useContinuationPrompt ? Shell.CONTINUATION_PROMPT : Shell.MAIN_PROMPT, EditorStyles.wordWrappedLabel, GUILayout.Width(37)); 417 | 418 | lnEditorState.text = codeToProcess; 419 | lnEditorState = UnityREPLHelper.NumberedTextArea(editorControlName, lnEditorState); 420 | codeToProcess = lnEditorState.text; 421 | GUILayout.EndHorizontal(); 422 | } 423 | 424 | private Hashtable fields = null; 425 | public Vector2 scrollPosition = Vector2.zero; 426 | 427 | private void ShowVars() { 428 | if(fields == null) 429 | fields = EvaluatorProxy.fields; 430 | 431 | scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, false, false); 432 | EditorGUI.indentLevel++; 433 | GUILayout.BeginHorizontal(); 434 | GUILayout.Space(EditorGUI.indentLevel * 14); 435 | GUILayout.BeginVertical(); 436 | // TODO: This is gonna be WAY inefficient *AND* ugly. Need a better 437 | // TODO: way to handle tabular data, and need a way to track what 438 | // TODO: has/hasn't changed here. 439 | StringBuilder tmp = new StringBuilder(); 440 | foreach(DictionaryEntry kvp in fields) { 441 | FieldInfo field = (FieldInfo)kvp.Value; 442 | GUILayout.BeginHorizontal(); 443 | GUILayout.Label(TypeManagerProxy.CSharpName(field.FieldType)); 444 | GUILayout.Space(10); 445 | GUILayout.Label((string)kvp.Key); 446 | GUILayout.FlexibleSpace(); 447 | PrettyPrint.PP(tmp, field.GetValue(null)); 448 | GUILayout.Label(tmp.ToString()); 449 | tmp.Length = 0; 450 | GUILayout.EndHorizontal(); 451 | } 452 | GUILayout.EndVertical(); 453 | GUILayout.EndHorizontal(); 454 | EditorGUI.indentLevel--; 455 | EditorGUILayout.EndScrollView(); 456 | } 457 | 458 | private const string editorControlName = "REPLEditor"; 459 | //---------------------------------------------------------------------------- 460 | 461 | //---------------------------------------------------------------------------- 462 | // Help Screen 463 | //---------------------------------------------------------------------------- 464 | public Vector2 helpScrollPosition = Vector2.zero; 465 | private bool showQuickStart = true, showEditing = true, showLogging = true, 466 | showShortcuts = true, showLocals = true, showKnownIssues = true, 467 | showExpressions = true, showLanguageFeatures = true; 468 | 469 | public void ShowHelp() { 470 | helpScrollPosition = EditorGUILayout.BeginScrollView(helpScrollPosition); 471 | GUILayout.Label("UnityREPL v." + Shell.VERSION, HelpStyles.Header); 472 | GUILayout.Label(Shell.COPYRIGHT, HelpStyles.Header); 473 | 474 | GUILayout.Label("", HelpStyles.Content); 475 | 476 | showQuickStart = EditorGUILayout.Foldout(showQuickStart, "Quick Start", HelpStyles.SubHeader); 477 | if(showQuickStart) { 478 | GUILayout.Label("Type your C# code into the main text area, and press when you're done." + 479 | " If your code has an error, or is incomplete, the prompt will change from '" + Shell.MAIN_PROMPT + 480 | "' to '" + Shell.CONTINUATION_PROMPT + "', and you'll be allowed to continue typing." + 481 | " If there's a logic-error that the C# compiler can detect at compile-time, it will be" + 482 | " written to the log pane as well.\n\nIf your code is correct, it will be executed immediately upon" + 483 | " pressing .\n\nPlease see the Known Issues section below, to avoid some frustrating corner-cases!", 484 | HelpStyles.Content); 485 | GUILayout.Label("", HelpStyles.Content); 486 | } 487 | 488 | showExpressions = EditorGUILayout.Foldout(showExpressions, "Expressions", HelpStyles.SubHeader); 489 | if(showExpressions) { 490 | GUILayout.Label("If you begin your code with an '=', and omit a trailing ';', then UnityREPL" + 491 | " will behave a little differently than usual, and will evaluate everything after the '=' as" + 492 | " an expression. The log pane will show both the expression you entered, and the result of the" + 493 | " evaluation. This can be handy for a quick calculator, or for peeking at data in detail. In" + 494 | " particular, if an expression evaluates to a Type object, you will be shown a the interface exposed" + 495 | " by that type in pseudo-C# syntax. Try these out:", HelpStyles.Content); 496 | 497 | GUILayout.Label("= 4 * 20", HelpStyles.Code); 498 | GUILayout.Label("= typeof(EditorApplication)", HelpStyles.Code); 499 | 500 | GUILayout.Label("", HelpStyles.Content); 501 | } 502 | 503 | showLanguageFeatures = EditorGUILayout.Foldout(showLanguageFeatures, "Language Features", HelpStyles.SubHeader); 504 | if(showLanguageFeatures) { 505 | GUILayout.Label("UnityREPL implements a newer version of the C# language than Unity itself supports, unless" + 506 | " you are using Unity 3.0 or newer. You get a couple nifty features for code entered into the interactive" + 507 | " editor...", HelpStyles.Content); 508 | GUILayout.Label("", HelpStyles.Content); 509 | GUILayout.Label("The 'var' keyword:", HelpStyles.Content); 510 | GUILayout.Label("var i = 3;", HelpStyles.Code); 511 | GUILayout.Label("", HelpStyles.Content); 512 | GUILayout.Label("Linq:", HelpStyles.Content); 513 | GUILayout.Label("= from f in Directory.GetFiles(Application.dataPath)\n" + 514 | " let fi = new FileInfo(f)\n" + 515 | " where fi.LastWriteTime > DateTime.Now - TimeSpan.FromDays(7)\n" + 516 | " select f", HelpStyles.Code); 517 | GUILayout.Label("", HelpStyles.Content); 518 | GUILayout.Label("Anonymous Types:", HelpStyles.Content); 519 | GUILayout.Label("var x = new { foo = \"blah\", bar = 123 };", HelpStyles.Code); 520 | GUILayout.Space(4); 521 | GUILayout.Label("... which you can access like so:", HelpStyles.Content); 522 | GUILayout.Label("= x.foo", HelpStyles.Code); 523 | GUILayout.Label("", HelpStyles.Content); 524 | } 525 | 526 | showEditing = EditorGUILayout.Foldout(showEditing, "Editing", HelpStyles.SubHeader); 527 | if(showEditing) { 528 | GUILayout.Label("The editor panel works like many familiar text editors, and supports using the " + 529 | " key to indent, unlike normal Unity text fields. Additionally, there are keyboard shortcuts to" + 530 | " indent/unindent a single line or a selected block of lines. Cut/Copy/Paste are also fully supported.\n\n" + 531 | "If you wish to insert a line into your code, but pressing would execute it, you can press " + 532 | "- to suppress execution.", HelpStyles.Content); 533 | GUILayout.Label("", HelpStyles.Content); 534 | } 535 | 536 | showLogging = EditorGUILayout.Foldout(showLogging, "Logging", HelpStyles.SubHeader); 537 | if(showLogging) { 538 | GUILayout.Label("Any output sent to Debug.Log, Debug.LogWarning, or Debug.LogError during execution of" + 539 | " your code is captured and showed in the log pane, as well as the normal Unity console view. You can" + 540 | " disable this view by disabling the 'Log' toggle on the toolbar. You can also filter certain stack" + 541 | " trace elements that are unlikely to be useful by enabling the 'Filter' toggle on the toolbar. Any" + 542 | " code that takes the form of an expression will be evaluated and the result of the expression will" + 543 | " appear below it in the log in green. Any errors or exceptions will appear in red. Warnings in yellow." + 544 | " Normal log messages will appear in black or white depending on which Unity skin is" + 545 | " enabled.\n\nIf your code spans multiple lines, or there is any form of output associated with it, a" + 546 | " disclosure triangle will appear next to it, allowing you to collapse the log entry down to a single" + 547 | " line.\n\nFinally, a button with a '+' on it appears next to the part of the log entry showing code" + 548 | " you've executed. Pressing this button will replace the contents of the editor field with that code," + 549 | " so you can run it again.", HelpStyles.Content); 550 | GUILayout.Label("", HelpStyles.Content); 551 | } 552 | 553 | showLocals = EditorGUILayout.Foldout(showLocals, "Locals", HelpStyles.SubHeader); 554 | if(showLocals) { 555 | GUILayout.Label("The locals pane will show you the type, name, and current value of any variables your" + 556 | " code creates. These variables will persist across multiple snippets of code, which can be very helpful" + 557 | " for breaking tasks up into separate steps.", HelpStyles.Content); 558 | GUILayout.Label("", HelpStyles.Content); 559 | } 560 | 561 | showShortcuts = EditorGUILayout.Foldout(showShortcuts, "Keyboard Shortcuts", HelpStyles.SubHeader); 562 | if(showShortcuts) { 563 | GUILayout.BeginHorizontal(); 564 | if(Application.platform == RuntimePlatform.OSXEditor) 565 | GUILayout.Label("--R", HelpStyles.Shortcut); 566 | else 567 | GUILayout.Label("--R", HelpStyles.Shortcut); 568 | GUILayout.Label("Switch to the UnityREPL window (opening one if needed).", HelpStyles.Explanation); 569 | GUILayout.EndHorizontal(); 570 | GUILayout.BeginHorizontal(); 571 | GUILayout.Label("-", HelpStyles.Shortcut); 572 | GUILayout.Label("Insert a new line, without submitting the code for execution.", HelpStyles.Explanation); 573 | GUILayout.EndHorizontal(); 574 | GUILayout.BeginHorizontal(); 575 | if(Application.platform == RuntimePlatform.OSXEditor) 576 | GUILayout.Label("-]", HelpStyles.Shortcut); 577 | else 578 | GUILayout.Label("-]", HelpStyles.Shortcut); 579 | GUILayout.Label("If text is selected, indent all of it. If there is no text selected, indent the current line.", HelpStyles.Explanation); 580 | GUILayout.EndHorizontal(); 581 | GUILayout.BeginHorizontal(); 582 | if(Application.platform == RuntimePlatform.OSXEditor) 583 | GUILayout.Label("-[", HelpStyles.Shortcut); 584 | else 585 | GUILayout.Label("-[", HelpStyles.Shortcut); 586 | GUILayout.Label("If text is selected, un-indent all of it. If there is no text selected, un-indent the current line.", HelpStyles.Explanation); 587 | GUILayout.EndHorizontal(); 588 | GUILayout.Label("", HelpStyles.Content); 589 | } 590 | 591 | showKnownIssues = EditorGUILayout.Foldout(showKnownIssues, "Known Issues", HelpStyles.SubHeader); 592 | if(showKnownIssues) { 593 | GUILayout.Label("Any 'using' statements must be executed by themselves, as separate and individual code snippets.", HelpStyles.Content); 594 | GUILayout.Label("Locals are wiped when Unity recompiles your code, or when entering play-mode.", HelpStyles.Content); 595 | GUILayout.Label("Locals view cannot display the name of a generic type.", HelpStyles.Content); 596 | GUILayout.Label("", HelpStyles.Content); 597 | } 598 | 599 | GUILayout.Space(4); 600 | EditorGUILayout.EndScrollView(); 601 | } 602 | //---------------------------------------------------------------------------- 603 | 604 | //---------------------------------------------------------------------------- 605 | // Tying It All Together... 606 | //---------------------------------------------------------------------------- 607 | public bool showVars = true, showHelp = false; 608 | 609 | public void OnGUI() { 610 | HandleInputFocusAndStateForEditor(); 611 | 612 | EditorGUILayoutToolbar.Begin(); 613 | showVars = GUILayout.Toggle(showVars, "Locals", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)); 614 | 615 | EditorGUILayoutToolbar.FlexibleSpace(); 616 | 617 | showHelp = GUILayout.Toggle(showHelp, "?", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)); 618 | 619 | EditorGUILayoutToolbar.End(); 620 | 621 | if(showHelp) 622 | ShowHelp(); 623 | else { 624 | ShowEditor(); 625 | 626 | if(showVars) 627 | ShowVars(); 628 | } 629 | } 630 | 631 | [MenuItem("Window/C# Shell #%r")] 632 | public static void Init() { 633 | Shell window = (Shell)EditorWindow.GetWindow(typeof(Shell)); 634 | window.titleContent = new GUIContent("C# Shell"); 635 | window.Show(); 636 | } 637 | } 638 | -------------------------------------------------------------------------------- /Shell.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 23dc97b5f6f054e4eacaa063c823b683 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | userData: 9 | -------------------------------------------------------------------------------- /UnityREPLHelper.cs: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------- 2 | // Helper for editor-related UnityGUI tasks. 3 | //----------------------------------------------------------------- 4 | using UnityEngine; 5 | using UnityEditor; 6 | using System.Text; 7 | using System.Collections; 8 | 9 | public class UnityREPLHelper { 10 | private static Hashtable styleCache = new Hashtable(); 11 | 12 | public static GUIStyle CachedStyle(string name) { 13 | if(!styleCache.ContainsKey(name)) 14 | styleCache[name] = GUI.skin.GetStyle(name); 15 | return (GUIStyle)styleCache[name]; 16 | } 17 | 18 | public static NumberedEditorState NumberedTextArea(string controlName, NumberedEditorState editorState) { 19 | // This is a WAG about Unity's box model. Seems to work though, so... yeah. 20 | float effectiveWidgetHeight = 7 * GUI.skin.label.lineHeight 21 | + GUI.skin.label.padding.top + GUI.skin.label.padding.bottom; 22 | Rect r = EditorGUILayout.BeginVertical(); 23 | if(r.width > 0) { 24 | editorState.scrollViewWidth = r.width; 25 | editorState.scrollViewHeight = r.height; 26 | } 27 | 28 | editorState.scrollPos = GUILayout.BeginScrollView(editorState.scrollPos, false, false, CachedStyle("HorizontalScrollbar"), CachedStyle("VerticalScrollbar"), CachedStyle("TextField"), GUILayout.Height(effectiveWidgetHeight)); 29 | GUILayout.BeginHorizontal(); 30 | GUILayout.Label(editorState.lineNumberingContent, NumberedEditorStyles.LineNumbering); 31 | GUIContent txt = new GUIContent(editorState.text); 32 | GUIContent dTxt = new GUIContent(editorState.dummyText); 33 | float minW, maxW; 34 | NumberedEditorStyles.NumberedEditor.CalcMinMaxWidth(dTxt, out minW, out maxW); 35 | GUI.SetNextControlName(controlName); 36 | Rect editorRect = GUILayoutUtility.GetRect(txt, NumberedEditorStyles.NumberedEditor, GUILayout.Width(maxW)); 37 | editorRect.width = maxW; 38 | bool wasMouseDrag = Event.current.type == EventType.MouseDrag; 39 | bool wasRelevantEvent = wasMouseDrag || Event.current.type == EventType.KeyDown; 40 | editorState.text = GUI.TextField(editorRect, editorState.text, NumberedEditorStyles.NumberedEditor); 41 | 42 | if((GUI.GetNameOfFocusedControl() == controlName) && 43 | wasRelevantEvent) { 44 | int editorId = GUIUtility.keyboardControl; 45 | TextEditor te = GUIUtility.QueryStateObject(typeof(System.Object), editorId) as TextEditor; 46 | int pos = te.cursorIndex; // TODO: How does this play with keyboard selection? We want the actual cursor pos, not necessarily the right-end. 47 | if(pos != editorState.lastPos) { 48 | Vector2 cursorPixelPos = NumberedEditorStyles.NumberedEditor.GetCursorPixelPosition(editorRect, txt, pos); 49 | cursorPixelPos.y -= 1; // 0-align... 50 | float yBuffer = NumberedEditorStyles.NumberedEditor.lineHeight * 2; 51 | float xBuffer = 40f; // TODO: Make this a little less arbitrary? 52 | if(wasMouseDrag) { 53 | yBuffer = 0; 54 | xBuffer = 0; 55 | } 56 | 57 | if(editorState.scrollViewWidth > 0) { 58 | if(cursorPixelPos.y + yBuffer > editorState.scrollPos.y + editorState.scrollViewHeight - NumberedEditorStyles.NumberedEditor.lineHeight) 59 | editorState.scrollPos.y = cursorPixelPos.y + yBuffer + NumberedEditorStyles.NumberedEditor.lineHeight - editorState.scrollViewHeight; 60 | if(cursorPixelPos.y - yBuffer < editorState.scrollPos.y) 61 | editorState.scrollPos.y = cursorPixelPos.y - yBuffer; 62 | 63 | if(cursorPixelPos.x + xBuffer > editorState.scrollPos.x + editorState.scrollViewWidth) 64 | editorState.scrollPos.x = cursorPixelPos.x + xBuffer - editorState.scrollViewWidth; 65 | if(cursorPixelPos.x - xBuffer < editorState.scrollPos.x) 66 | editorState.scrollPos.x = cursorPixelPos.x - xBuffer; 67 | } 68 | } 69 | editorState.lastPos = pos; 70 | } 71 | GUILayout.EndHorizontal(); 72 | GUILayout.EndScrollView(); 73 | EditorGUILayout.EndVertical(); 74 | 75 | return editorState; 76 | } 77 | } 78 | 79 | public class HelpStyles { 80 | private static GUIStyle _Header = null; 81 | 82 | public static GUIStyle Header { 83 | get { 84 | if(_Header == null) { 85 | _Header = new GUIStyle(EditorStyles.largeLabel) { 86 | alignment = TextAnchor.UpperLeft, 87 | wordWrap = true, 88 | imagePosition = ImagePosition.TextOnly 89 | } 90 | .Named("HelpHeader") 91 | .Size(0, 0, false, false) 92 | .NoBackgroundImages() 93 | .ResetBoxModel() 94 | .Margin(4, 4, 0, 0) 95 | .ClipText(); 96 | } 97 | return _Header; 98 | } 99 | } 100 | 101 | private static GUIStyle _SubHeader = null; 102 | 103 | public static GUIStyle SubHeader { 104 | get { 105 | if(_SubHeader == null) { 106 | _SubHeader = new GUIStyle(EditorStyles.foldout) { 107 | alignment = TextAnchor.UpperLeft, 108 | wordWrap = false, 109 | imagePosition = ImagePosition.TextOnly, 110 | font = EditorStyles.boldLabel.font 111 | } 112 | .Named("HelpSubHeader") 113 | .Size(0, 0, true, false) 114 | .Margin(4, 4, 0, 0); 115 | } 116 | return _SubHeader; 117 | } 118 | } 119 | 120 | private static GUIStyle _Content = null; 121 | 122 | public static GUIStyle Content { 123 | get { 124 | if(_Content == null) { 125 | _Content = new GUIStyle(EditorStyles.wordWrappedLabel) { 126 | alignment = TextAnchor.UpperLeft, 127 | wordWrap = true, 128 | imagePosition = ImagePosition.TextOnly 129 | } 130 | .Named("HelpContent") 131 | .Size(0, 0, false, false) 132 | .NoBackgroundImages() 133 | .ResetBoxModel() 134 | .Margin(4 + 14, 4, 0, 0) 135 | .ClipText(); 136 | } 137 | return _Content; 138 | } 139 | } 140 | 141 | private static GUIStyle _Code = null; 142 | 143 | public static GUIStyle Code { 144 | get { 145 | if(_Code == null) { 146 | _Code = new GUIStyle(EditorStyles.boldLabel) { 147 | alignment = TextAnchor.UpperLeft, 148 | wordWrap = false, 149 | imagePosition = ImagePosition.TextOnly 150 | } 151 | .Named("HelpCode") 152 | .Size(0, 0, false, false) 153 | .NoBackgroundImages() 154 | .ResetBoxModel() 155 | .Margin(4 + 14 + 10, 4, 4, 0) 156 | .ClipText(); 157 | } 158 | return _Code; 159 | } 160 | } 161 | 162 | private static GUIStyle _Shortcut = null; 163 | 164 | public static GUIStyle Shortcut { 165 | get { 166 | if(_Shortcut == null) { 167 | _Shortcut = new GUIStyle(EditorStyles.boldLabel) { 168 | alignment = TextAnchor.UpperLeft, 169 | wordWrap = false, 170 | imagePosition = ImagePosition.TextOnly 171 | } 172 | .Named("HelpShortcut") 173 | .Size(120, 0, false, false) 174 | .NoBackgroundImages() 175 | .ResetBoxModel() 176 | .Margin(4 + 14 + 10, 0, 0, 0) 177 | .ClipText(); 178 | } 179 | return _Shortcut; 180 | } 181 | } 182 | 183 | private static GUIStyle _Explanation = null; 184 | 185 | public static GUIStyle Explanation { 186 | get { 187 | if(_Explanation == null) { 188 | _Explanation = new GUIStyle(EditorStyles.label) { 189 | alignment = TextAnchor.UpperLeft, 190 | wordWrap = true, 191 | imagePosition = ImagePosition.TextOnly 192 | } 193 | .Named("HelpExplanation") 194 | .Size(0, 0, false, false) 195 | .NoBackgroundImages() 196 | .ResetBoxModel() 197 | .Margin(0, 4, 0, 0) 198 | .ClipText(); 199 | } 200 | return _Explanation; 201 | } 202 | } 203 | 204 | } 205 | 206 | public class NumberedEditorStyles { 207 | private static GUIStyle _NumberedEditor = null; 208 | 209 | public static GUIStyle NumberedEditor { 210 | get { 211 | if(_NumberedEditor == null) { 212 | _NumberedEditor = new GUIStyle(EditorStyles.textField) { 213 | alignment = TextAnchor.UpperLeft, 214 | wordWrap = false, 215 | imagePosition = ImagePosition.TextOnly 216 | } 217 | .Named("NumberedEditor") 218 | .Size(0, 0, true, true) 219 | .NoBackgroundImages() 220 | .ResetBoxModel() 221 | .Padding(0, 4, 0, 0) 222 | .Margin(5, 0, 0, 0) 223 | .ClipText(); 224 | } 225 | return _NumberedEditor; 226 | } 227 | } 228 | 229 | private static GUIStyle _LineNumbering = null; 230 | 231 | public static GUIStyle LineNumbering { 232 | get { 233 | if(_LineNumbering == null) { 234 | _LineNumbering = new GUIStyle(NumberedEditor) { 235 | alignment = TextAnchor.UpperRight 236 | } 237 | .Named("LineNumbering") 238 | .Size(0, 0, false, true) 239 | .Padding(5, 0, 0, 0) 240 | .Margin(0, 0, 0, 0) 241 | .BaseTextColor(new Color(0.5f, 0.5f, 0.5f, 1f)); 242 | } 243 | return _LineNumbering; 244 | } 245 | } 246 | } 247 | 248 | public class LogEntryStyles { 249 | private static GUIStyle[] styles = new GUIStyle[_Count]; 250 | private const int _Default = 0; 251 | 252 | public static GUIStyle Default { 253 | get { 254 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 255 | if(styles[_Default] == null) { 256 | styles[_Default] = new GUIStyle(EditorStyles.label) 257 | .Named("DefaultLogEntry") 258 | .ResetBoxModel() 259 | .Padding(2, 2, 2, 2) 260 | .Size(0, 0, true, false); 261 | } 262 | return styles[_Default]; 263 | } 264 | } 265 | 266 | private const int _DefaultCommandStyle = 1; 267 | 268 | public static GUIStyle DefaultCommandStyle { 269 | get { 270 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 271 | if(styles[_DefaultCommandStyle] == null) { 272 | styles[_DefaultCommandStyle] = new GUIStyle(Default) 273 | .Named("DefaultCommandStyle"); 274 | } 275 | return styles[_DefaultCommandStyle]; 276 | } 277 | } 278 | 279 | private const int _FoldoutCommandStyle = 2; 280 | 281 | public static GUIStyle FoldoutCommandStyle { 282 | get { 283 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 284 | if(styles[_FoldoutCommandStyle] == null) { 285 | styles[_FoldoutCommandStyle] = new GUIStyle(EditorStyles.foldout) 286 | .Named("FoldoutCommandStyle") 287 | .BaseTextColor(DefaultCommandStyle.active.textColor); 288 | } 289 | return styles[_FoldoutCommandStyle]; 290 | } 291 | } 292 | 293 | private const int _FoldoutCopyContentStyle = 3; 294 | 295 | public static GUIStyle FoldoutCopyContentStyle { 296 | get { 297 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 298 | if(styles[_FoldoutCopyContentStyle] == null) 299 | styles[_FoldoutCopyContentStyle] = new GUIStyle(EditorGUIUtility.GetBuiltinSkin(EditorSkin.Inspector).GetStyle("OL Plus")); 300 | return styles[_FoldoutCopyContentStyle]; 301 | } 302 | } 303 | 304 | private const int PLAIN_TEXT = 0, 305 | ERROR_TEXT = 1, 306 | WARNING_TEXT = 2; 307 | private static Color[,] COLORS = new Color[,] { 308 | { new Color(0f, 0f, 0f, 1f), new Color(0.75f, 0.75f, 0.75f, 1f) }, 309 | { new Color(0.5f, 0f, 0f, 1f), new Color(1f, 0.25f, 0.25f, 1f) }, 310 | { new Color(0.4f, 0.3f, 0f, 1f), new Color(1f, 0.7f, 0f, 1f) } 311 | }; 312 | private const int _OutputStyle = 4; 313 | 314 | public static GUIStyle OutputStyle { 315 | get { 316 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 317 | if(styles[_OutputStyle] == null) { 318 | styles[_OutputStyle] = new GUIStyle(Default) 319 | .Named("OutputStyle") 320 | .BaseTextColor(COLORS[PLAIN_TEXT, 0], COLORS[PLAIN_TEXT, 1]); 321 | } 322 | return styles[_OutputStyle]; 323 | } 324 | } 325 | 326 | private const int _EvaluationErrorStyle = 5; 327 | 328 | public static GUIStyle EvaluationErrorStyle { 329 | get { 330 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 331 | if(styles[_EvaluationErrorStyle] == null) { 332 | styles[_EvaluationErrorStyle] = new GUIStyle(Default) 333 | .Named("EvaluationErrorStyle") 334 | .BaseTextColor(COLORS[ERROR_TEXT, 0], COLORS[ERROR_TEXT, 1]); 335 | } 336 | return styles[_EvaluationErrorStyle]; 337 | } 338 | } 339 | 340 | private const int _SystemConsoleOutStyle = 6; 341 | 342 | public static GUIStyle SystemConsoleOutStyle { 343 | get { 344 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 345 | if(styles[_SystemConsoleOutStyle] == null) { 346 | styles[_SystemConsoleOutStyle] = new GUIStyle(Default) 347 | .Named("SystemConsoleOutStyle") 348 | .BaseTextColor(COLORS[WARNING_TEXT, 0], COLORS[WARNING_TEXT, 1]); 349 | } 350 | return styles[_SystemConsoleOutStyle]; 351 | } 352 | } 353 | 354 | private const int _SystemConsoleErrStyle = 7; 355 | 356 | public static GUIStyle SystemConsoleErrStyle { 357 | get { 358 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 359 | if(styles[_SystemConsoleErrStyle] == null) { 360 | styles[_SystemConsoleErrStyle] = new GUIStyle(Default) 361 | .Named("SystemConsoleErrStyle") 362 | .BaseTextColor(COLORS[ERROR_TEXT, 0], COLORS[ERROR_TEXT, 1]); 363 | } 364 | return styles[_SystemConsoleErrStyle]; 365 | } 366 | } 367 | 368 | private const int _ConsoleLogStyle = 8; 369 | 370 | public static GUIStyle ConsoleLogStyle { 371 | get { 372 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 373 | if(styles[_ConsoleLogStyle] == null) { 374 | styles[_ConsoleLogStyle] = new GUIStyle(Default) 375 | .Named("ConsoleLogStyle"); 376 | } 377 | return styles[_ConsoleLogStyle]; 378 | } 379 | } 380 | 381 | private const int _ConsoleLogNormalStyle = 9; 382 | 383 | public static GUIStyle ConsoleLogNormalStyle { 384 | get { 385 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 386 | if(styles[_ConsoleLogNormalStyle] == null) { 387 | styles[_ConsoleLogNormalStyle] = new GUIStyle(ConsoleLogStyle) 388 | .Named("ConsoleLogNormalStyle"); 389 | } 390 | return styles[_ConsoleLogNormalStyle]; 391 | } 392 | } 393 | 394 | private const int _ConsoleLogWarningStyle = 10; 395 | 396 | public static GUIStyle ConsoleLogWarningStyle { 397 | get { 398 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 399 | if(styles[_ConsoleLogWarningStyle] == null) { 400 | styles[_ConsoleLogWarningStyle] = new GUIStyle(ConsoleLogStyle) 401 | .Named("ConsoleLogWarningStyle") 402 | .BaseTextColor(COLORS[WARNING_TEXT, 0], COLORS[WARNING_TEXT, 1]); 403 | } 404 | return styles[_ConsoleLogWarningStyle]; 405 | } 406 | } 407 | 408 | private const int _ConsoleLogErrorStyle = 11; 409 | 410 | public static GUIStyle ConsoleLogErrorStyle { 411 | get { 412 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 413 | if(styles[_ConsoleLogErrorStyle] == null) { 414 | styles[_ConsoleLogErrorStyle] = new GUIStyle(ConsoleLogStyle) 415 | .Named("ConsoleLogErrorStyle") 416 | .BaseTextColor(COLORS[ERROR_TEXT, 0], COLORS[ERROR_TEXT, 1]); 417 | } 418 | return styles[_ConsoleLogErrorStyle]; 419 | } 420 | } 421 | 422 | private const int _ConsoleLogStackTraceStyle = 12; 423 | 424 | public static GUIStyle ConsoleLogStackTraceStyle { 425 | get { 426 | EditorGUIStyleExtensions.InvalidateOnSkinChange(styles); 427 | if(styles[_ConsoleLogStackTraceStyle] == null) { 428 | styles[_ConsoleLogStackTraceStyle] = new GUIStyle(ConsoleLogStyle) 429 | .Named("ConsoleLogStackTraceStyle") 430 | .BaseTextColor(COLORS[PLAIN_TEXT, 0], COLORS[PLAIN_TEXT, 1]); 431 | } 432 | return styles[_ConsoleLogStackTraceStyle]; 433 | } 434 | } 435 | 436 | private const int _Count = 13; 437 | } 438 | 439 | [System.Serializable] 440 | public class NumberedEditorState { 441 | public Vector2 scrollPos; 442 | public float scrollViewWidth, scrollViewHeight; 443 | public int lastPos; 444 | public bool textChanged = false; 445 | private string _text = ""; 446 | 447 | public string text { 448 | get { return _text; } 449 | set { 450 | if(_text != value) { 451 | _text = value; 452 | _lineNumberingContent = null; 453 | _textContent = null; 454 | _dummyText = null; 455 | textChanged = true; 456 | } 457 | } 458 | } 459 | 460 | private GUIContent _textContent = null; 461 | 462 | public GUIContent textContent { 463 | get { 464 | if(_textContent == null) 465 | _textContent = new GUIContent(text); 466 | return _textContent; 467 | } 468 | } 469 | 470 | private string _dummyText = null; 471 | 472 | public string dummyText { 473 | get { 474 | return _dummyText; 475 | } 476 | } 477 | 478 | private GUIContent _lineNumberingContent = null; 479 | 480 | public GUIContent lineNumberingContent { 481 | get { 482 | // Unity likes to ignore trailing space when sizing content, which is a 483 | // problem for us, so we construct a version of our content that has a . 484 | // at the end of each line -- small enough to not consume too much extra 485 | // width, but not a space, so we can use that for sizing later on. 486 | if(_lineNumberingContent == null) { 487 | string[] linesRaw = text.Split('\n'); 488 | int lines = linesRaw.Length; 489 | if(lines == 0) 490 | lines = 1; 491 | 492 | StringBuilder sb = new StringBuilder(); 493 | for(int j = 0; j < lines; j++) 494 | sb.Append(linesRaw[j]).Append(".").Append("\n"); 495 | _dummyText = sb.ToString(); 496 | 497 | // While we're at it, build a single string with all our line numbers. 498 | sb.Length = 0; 499 | for(int j = 0; j < lines; j++) 500 | sb.Append(j + 1).Append('\n'); 501 | 502 | _lineNumberingContent = new GUIContent(sb.ToString()); 503 | } 504 | return _lineNumberingContent; 505 | } 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /UnityREPLHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 101cd6d862669488e9aa33fb156070f2 3 | MonoImporter: 4 | serializedVersion: 2 5 | defaultReferences: [] 6 | executionOrder: 0 7 | icon: {instanceID: 0} 8 | --------------------------------------------------------------------------------