Namespace cleanup, move some categories out of UI namespace

This commit is contained in:
Sinai
2021-06-30 07:49:58 +10:00
parent 65c4d49274
commit f815a13d9a
55 changed files with 174 additions and 150 deletions

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityExplorer.CSConsole.Lexers;
using UnityExplorer.UI;
using UnityExplorer.UI.Widgets.AutoComplete;
namespace UnityExplorer.CSConsole
{
public class CSAutoCompleter : ISuggestionProvider
{
public InputFieldRef InputField => ConsoleController.Input;
public bool AnchorToCaretPosition => true;
bool ISuggestionProvider.AllowNavigation => true;
public void OnSuggestionClicked(Suggestion suggestion)
{
ConsoleController.InsertSuggestionAtCaret(suggestion.UnderlyingValue);
AutoCompleteModal.Instance.ReleaseOwnership(this);
}
private readonly HashSet<char> delimiters = new HashSet<char>
{
'{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?'
};
private readonly List<Suggestion> suggestions = new List<Suggestion>();
public void CheckAutocompletes()
{
if (string.IsNullOrEmpty(InputField.Text))
{
AutoCompleteModal.Instance.ReleaseOwnership(this);
return;
}
suggestions.Clear();
int caret = Math.Max(0, Math.Min(InputField.Text.Length - 1, InputField.Component.caretPosition - 1));
int startIdx = caret;
// If the character at the caret index is whitespace or delimiter,
// or if the next character (if it exists) is not whitespace,
// then we don't want to provide suggestions.
if (char.IsWhiteSpace(InputField.Text[caret])
|| delimiters.Contains(InputField.Text[caret])
|| (InputField.Text.Length > caret + 1 && !char.IsWhiteSpace(InputField.Text[caret + 1])))
{
AutoCompleteModal.Instance.ReleaseOwnership(this);
return;
}
// get the current composition string (from caret back to last delimiter)
while (startIdx > 0)
{
startIdx--;
char c = InputField.Text[startIdx];
if (delimiters.Contains(c) || char.IsWhiteSpace(c))
{
startIdx++;
break;
}
}
string input = InputField.Text.Substring(startIdx, caret - startIdx + 1);
// Get MCS completions
string[] evaluatorCompletions = ConsoleController.Evaluator.GetCompletions(input, out string prefix);
if (evaluatorCompletions != null && evaluatorCompletions.Any())
{
suggestions.AddRange(from completion in evaluatorCompletions
select new Suggestion(GetHighlightString(prefix, completion), completion));
}
// Get manual namespace completions
foreach (var ns in ReflectionUtility.AllNamespaces)
{
if (ns.StartsWith(input))
{
if (!namespaceHighlights.ContainsKey(ns))
namespaceHighlights.Add(ns, $"<color=#CCCCCC>{ns}</color>");
string completion = ns.Substring(input.Length, ns.Length - input.Length);
suggestions.Add(new Suggestion(namespaceHighlights[ns], completion));
}
}
// Get manual keyword completions
foreach (var kw in KeywordLexer.keywords)
{
if (kw.StartsWith(input))// && kw.Length > input.Length)
{
if (!keywordHighlights.ContainsKey(kw))
keywordHighlights.Add(kw, $"<color=#{SignatureHighlighter.keywordBlueHex}>{kw}</color>");
string completion = kw.Substring(input.Length, kw.Length - input.Length);
suggestions.Add(new Suggestion(keywordHighlights[kw], completion));
}
}
if (suggestions.Any())
{
AutoCompleteModal.Instance.TakeOwnership(this);
AutoCompleteModal.Instance.SetSuggestions(suggestions);
}
else
{
AutoCompleteModal.Instance.ReleaseOwnership(this);
}
}
private readonly Dictionary<string, string> namespaceHighlights = new Dictionary<string, string>();
private readonly Dictionary<string, string> keywordHighlights = new Dictionary<string, string>();
private readonly StringBuilder highlightBuilder = new StringBuilder();
private const string OPEN_HIGHLIGHT = "<color=cyan>";
private string GetHighlightString(string prefix, string completion)
{
highlightBuilder.Clear();
highlightBuilder.Append(OPEN_HIGHLIGHT);
highlightBuilder.Append(prefix);
highlightBuilder.Append(SignatureHighlighter.CLOSE_COLOR);
highlightBuilder.Append(completion);
return highlightBuilder.ToString();
}
}
}

View File

@ -0,0 +1,692 @@
using Mono.CSharp;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.Core.Input;
using UnityExplorer.CSConsole;
using UnityExplorer.UI;
using UnityExplorer.UI.Panels;
using UnityExplorer.UI.Widgets.AutoComplete;
namespace UnityExplorer.CSConsole
{
public static class ConsoleController
{
public static ScriptEvaluator Evaluator;
public static LexerBuilder Lexer;
public static CSAutoCompleter Completer;
private static HashSet<string> usingDirectives;
private static StringBuilder evaluatorOutput;
public static CSConsolePanel Panel => UIManager.GetPanel<CSConsolePanel>(UIManager.Panels.CSConsole);
public static InputFieldRef Input => Panel.Input;
public static int LastCaretPosition { get; private set; }
internal static float defaultInputFieldAlpha;
// Todo save as config?
public static bool EnableCtrlRShortcut { get; private set; } = true;
public static bool EnableAutoIndent { get; private set; } = true;
public static bool EnableSuggestions { get; private set; } = true;
internal static string ScriptsFolder => Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Scripts");
internal static readonly string[] DefaultUsing = new string[]
{
"System",
"System.Linq",
"System.Text",
"System.Collections.Generic",
"UnityEngine",
#if CPP
"UnhollowerBaseLib",
"UnhollowerRuntimeLib",
#endif
};
public static void Init()
{
// Make sure console is supported on this platform
try
{
ResetConsole(false);
// ensure the compiler is supported (if this fails then SRE is probably stubbed)
Evaluator.Compile("0 == 0");
}
catch (Exception ex)
{
DisableConsole(ex);
return;
}
// Setup console
Lexer = new LexerBuilder();
Completer = new CSAutoCompleter();
SetupHelpInteraction();
Panel.OnInputChanged += OnInputChanged;
Panel.InputScroller.OnScroll += OnInputScrolled;
Panel.OnCompileClicked += Evaluate;
Panel.OnResetClicked += ResetConsole;
Panel.OnHelpDropdownChanged += HelpSelected;
Panel.OnAutoIndentToggled += OnToggleAutoIndent;
Panel.OnCtrlRToggled += OnToggleCtrlRShortcut;
Panel.OnSuggestionsToggled += OnToggleSuggestions;
Panel.OnPanelResized += OnInputScrolled;
// Run startup script
try
{
if (!Directory.Exists(ScriptsFolder))
Directory.CreateDirectory(ScriptsFolder);
var startupPath = Path.Combine(ScriptsFolder, "startup.cs");
if (File.Exists(startupPath))
{
ExplorerCore.Log($"Executing startup script from '{startupPath}'...");
var text = File.ReadAllText(startupPath);
Input.Text = text;
Evaluate();
}
}
catch (Exception ex)
{
ExplorerCore.LogWarning($"Exception executing startup script: {ex}");
}
}
#region UI Listeners and options
// TODO save
private static void OnToggleAutoIndent(bool value)
{
EnableAutoIndent = value;
}
private static void OnToggleCtrlRShortcut(bool value)
{
EnableCtrlRShortcut = value;
}
private static void OnToggleSuggestions(bool value)
{
EnableSuggestions = value;
}
#endregion
#region Evaluating
public static void ResetConsole() => ResetConsole(true);
public static void ResetConsole(bool logSuccess = true)
{
if (SRENotSupported)
return;
if (Evaluator != null)
Evaluator.Dispose();
evaluatorOutput = new StringBuilder();
Evaluator = new ScriptEvaluator(new StringWriter(evaluatorOutput))
{
InteractiveBaseClass = typeof(ScriptInteraction)
};
usingDirectives = new HashSet<string>();
foreach (var use in DefaultUsing)
AddUsing(use);
if (logSuccess)
ExplorerCore.Log($"C# Console reset. Using directives:\r\n{Evaluator.GetUsing()}");
}
public static void AddUsing(string assemblyName)
{
if (!usingDirectives.Contains(assemblyName))
{
Evaluate($"using {assemblyName};", true);
usingDirectives.Add(assemblyName);
}
}
public static void Evaluate()
{
if (SRENotSupported)
return;
Evaluate(Input.Text);
}
public static void Evaluate(string input, bool supressLog = false)
{
if (SRENotSupported)
return;
try
{
// Compile the code. If it returned a CompiledMethod, it is REPL.
CompiledMethod repl = Evaluator.Compile(input);
if (repl != null)
{
// Valid REPL, we have a delegate to the evaluation.
try
{
object ret = null;
repl.Invoke(ref ret);
var result = ret?.ToString();
if (!string.IsNullOrEmpty(result))
ExplorerCore.Log($"Invoked REPL, result: {ret}");
else
ExplorerCore.Log($"Invoked REPL (no return value)");
}
catch (Exception ex)
{
ExplorerCore.LogWarning($"Exception invoking REPL: {ex}");
}
}
else
{
// The compiled code was not REPL, so it was a using directive or it defined classes.
string output = ScriptEvaluator._textWriter.ToString();
var outputSplit = output.Split('\n');
if (outputSplit.Length >= 2)
output = outputSplit[outputSplit.Length - 2];
evaluatorOutput.Clear();
if (ScriptEvaluator._reportPrinter.ErrorsCount > 0)
throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}");
else if (!supressLog)
ExplorerCore.Log($"Code compiled without errors.");
}
}
catch (FormatException fex)
{
if (!supressLog)
ExplorerCore.LogWarning(fex.Message);
}
catch (Exception ex)
{
if (!supressLog)
ExplorerCore.LogWarning(ex);
}
}
#endregion
// Updating and event listeners
private static bool settingCaretCoroutine;
private static void OnInputScrolled() => HighlightVisibleInput();
private static string previousInput;
// Invoked at most once per frame
private static void OnInputChanged(string value)
{
if (SRENotSupported)
return;
// prevent escape wiping input
if (InputManager.GetKeyDown(KeyCode.Escape))
{
Input.Text = previousInput;
if (EnableSuggestions && AutoCompleteModal.CheckEscape(Completer))
OnAutocompleteEscaped();
return;
}
previousInput = value;
if (EnableSuggestions && AutoCompleteModal.CheckEnter(Completer))
OnAutocompleteEnter();
if (!settingCaretCoroutine)
{
if (EnableAutoIndent)
DoAutoIndent();
}
var inStringOrComment = HighlightVisibleInput();
if (!settingCaretCoroutine)
{
if (EnableSuggestions)
{
if (inStringOrComment)
AutoCompleteModal.Instance.ReleaseOwnership(Completer);
else
Completer.CheckAutocompletes();
}
}
UpdateCaret(out _);
}
private static float timeOfLastCtrlR;
public static void Update()
{
if (SRENotSupported)
return;
UpdateCaret(out bool caretMoved);
if (!settingCaretCoroutine && EnableSuggestions)
{
if (AutoCompleteModal.CheckEscape(Completer))
{
OnAutocompleteEscaped();
return;
}
if (caretMoved)
AutoCompleteModal.Instance.ReleaseOwnership(Completer);
}
if (EnableCtrlRShortcut
&& (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl))
&& InputManager.GetKeyDown(KeyCode.R)
&& timeOfLastCtrlR.OccuredEarlierThanDefault())
{
timeOfLastCtrlR = Time.realtimeSinceStartup;
Evaluate(Panel.Input.Text);
}
}
private const int CSCONSOLE_LINEHEIGHT = 18;
private static void UpdateCaret(out bool caretMoved)
{
int prevCaret = LastCaretPosition;
caretMoved = false;
// Override up/down arrow movement when autocompleting
if (EnableSuggestions && AutoCompleteModal.CheckNavigation(Completer))
{
Input.Component.caretPosition = LastCaretPosition;
return;
}
if (Input.Component.isFocused)
{
LastCaretPosition = Input.Component.caretPosition;
caretMoved = LastCaretPosition != prevCaret;
}
if (Input.Text.Length == 0)
return;
// If caret moved, ensure caret is visible in the viewport
if (caretMoved)
{
var charInfo = Input.TextGenerator.characters[LastCaretPosition];
var charTop = charInfo.cursorPos.y;
var charBot = charTop - CSCONSOLE_LINEHEIGHT;
var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f);
var viewportMax = viewportMin - Panel.InputScroller.ViewportRect.rect.height;
float diff = 0f;
if (charTop > viewportMin)
diff = charTop - viewportMin;
else if (charBot < viewportMax)
diff = charBot - viewportMax;
if (Math.Abs(diff) > 1)
{
var rect = Input.Rect;
rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, rect.anchoredPosition.y - diff);
}
}
}
private static void SetCaretPosition(int caretPosition)
{
settingCaretCoroutine = true;
Input.Component.readOnly = true;
RuntimeProvider.Instance.StartCoroutine(SetCaretCoroutine(caretPosition));
}
internal static PropertyInfo SelectionGuardProperty => selectionGuardPropInfo ?? GetSelectionGuardPropInfo();
private static PropertyInfo GetSelectionGuardPropInfo()
{
selectionGuardPropInfo = typeof(EventSystem).GetProperty("m_SelectionGuard");
if (selectionGuardPropInfo == null)
selectionGuardPropInfo = typeof(EventSystem).GetProperty("m_selectionGuard");
return selectionGuardPropInfo;
}
private static PropertyInfo selectionGuardPropInfo;
private static IEnumerator SetCaretCoroutine(int caretPosition)
{
var color = Input.Component.selectionColor;
color.a = 0f;
Input.Component.selectionColor = color;
try { EventSystem.current.SetSelectedGameObject(null, null); } catch { }
yield return null;
try { SelectionGuardProperty.SetValue(EventSystem.current, false, null); } catch { }
try { EventSystem.current.SetSelectedGameObject(Input.UIRoot, null); } catch { }
Input.Component.Select();
yield return null;
Input.Component.caretPosition = caretPosition;
Input.Component.selectionFocusPosition = caretPosition;
LastCaretPosition = Input.Component.caretPosition;
color.a = defaultInputFieldAlpha;
Input.Component.selectionColor = color;
Input.Component.readOnly = false;
settingCaretCoroutine = false;
}
#region Lexer Highlighting
/// <summary>
/// Returns true if caret is inside string or comment, false otherwise
/// </summary>
private static bool HighlightVisibleInput()
{
if (string.IsNullOrEmpty(Input.Text))
{
Panel.HighlightText.text = "";
Panel.LineNumberText.text = "1";
return false;
}
// Calculate the visible lines
int topLine = -1;
int bottomLine = -1;
// the top and bottom position of the viewport in relation to the text height
// they need the half-height adjustment to normalize against the 'line.topY' value.
var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f);
var viewportMax = viewportMin - Panel.InputScroller.ViewportRect.rect.height;
for (int i = 0; i < Input.TextGenerator.lineCount; i++)
{
var line = Input.TextGenerator.lines[i];
// if not set the top line yet, and top of line is below the viewport top
if (topLine == -1 && line.topY <= viewportMin)
topLine = i;
// if bottom of line is below the viewport bottom
if ((line.topY - line.height) >= viewportMax)
bottomLine = i;
}
topLine = Math.Max(0, topLine - 1);
bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1);
int startIdx = Input.TextGenerator.lines[topLine].startCharIdx;
int endIdx = (bottomLine >= Input.TextGenerator.lineCount - 1)
? Input.Text.Length - 1
: (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1);
// Highlight the visible text with the LexerBuilder
Panel.HighlightText.text = Lexer.BuildHighlightedString(Input.Text, startIdx, endIdx, topLine, LastCaretPosition, out bool ret);
// Set the line numbers
// determine true starting line number (not the same as the cached TextGenerator line numbers)
int realStartLine = 0;
for (int i = 0; i < startIdx; i++)
{
if (LexerBuilder.IsNewLine(Input.Text[i]))
realStartLine++;
}
realStartLine++;
char lastPrev = '\n';
var sb = new StringBuilder();
// append leading new lines for spacing (no point rendering line numbers we cant see)
for (int i = 0; i < topLine; i++)
sb.Append('\n');
// append the displayed line numbers
for (int i = topLine; i <= bottomLine; i++)
{
if (i > 0)
lastPrev = Input.Text[Input.TextGenerator.lines[i].startCharIdx - 1];
// previous line ended with a newline character, this is an actual new line.
if (LexerBuilder.IsNewLine(lastPrev))
{
sb.Append(realStartLine.ToString());
realStartLine++;
}
sb.Append('\n');
}
Panel.LineNumberText.text = sb.ToString();
return ret;
}
#endregion
#region Autocompletes
public static void InsertSuggestionAtCaret(string suggestion)
{
settingCaretCoroutine = true;
Input.Text = Input.Text.Insert(LastCaretPosition, suggestion);
SetCaretPosition(LastCaretPosition + suggestion.Length);
LastCaretPosition = Input.Component.caretPosition;
}
private static void OnAutocompleteEnter()
{
// Remove the new line
int lastIdx = Input.Component.caretPosition - 1;
Input.Text = Input.Text.Remove(lastIdx, 1);
// Use the selected suggestion
Input.Component.caretPosition = LastCaretPosition;
Completer.OnSuggestionClicked(AutoCompleteModal.SelectedSuggestion);
}
private static void OnAutocompleteEscaped()
{
AutoCompleteModal.Instance.ReleaseOwnership(Completer);
SetCaretPosition(LastCaretPosition);
}
#endregion
#region Auto indenting
private static int prevContentLen = 0;
private static void DoAutoIndent()
{
if (Input.Text.Length > prevContentLen)
{
int inc = Input.Text.Length - prevContentLen;
if (inc == 1)
{
int caret = Input.Component.caretPosition;
Input.Text = Lexer.IndentCharacter(Input.Text, ref caret);
Input.Component.caretPosition = caret;
LastCaretPosition = caret;
}
else
{
// todo indenting for copy+pasted content
//ExplorerCore.Log("Content increased by " + inc);
//var comp = Input.Text.Substring(PreviousCaretPosition, inc);
//ExplorerCore.Log("composition string: " + comp);
}
}
prevContentLen = Input.Text.Length;
}
#endregion
#region "Help" interaction
private static bool SRENotSupported;
private static void DisableConsole(Exception ex)
{
SRENotSupported = true;
Input.Component.readOnly = true;
Input.Component.textComponent.color = "5d8556".ToColor();
if (ex is NotSupportedException)
{
Input.Text = $@"The C# Console has been disabled because System.Reflection.Emit threw an exception: {ex.ReflectionExToString()}
If the game was built with Unity's stubbed netstandard 2.0 runtime, you can fix this with UnityDoorstop:
* Download the Unity Editor version that the game uses
* Navigate to the folder:
- Editor\Data\PlaybackEngines\windowsstandalonesupport\Variations\mono\Managed
- or, Editor\Data\MonoBleedingEdge\lib\mono\4.5
* Copy the mscorlib.dll and System.Reflection.Emit DLLs from the folder
* Make a subfolder in the folder that contains doorstop_config.ini
* Put the DLLs inside the subfolder
* Set the 'dllSearchPathOverride' in doorstop_config.ini to the subfolder name";
}
else
{
Input.Text = $@"The C# Console has been disabled because of an unknown error.
{ex}";
}
}
private static readonly Dictionary<string, string> helpDict = new Dictionary<string, string>();
public static void SetupHelpInteraction()
{
var drop = Panel.HelpDropdown;
helpDict.Add("Help", "");
helpDict.Add("Usings", HELP_USINGS);
helpDict.Add("REPL", HELP_REPL);
helpDict.Add("Classes", HELP_CLASSES);
helpDict.Add("Coroutines", HELP_COROUTINES);
foreach (var opt in helpDict)
drop.options.Add(new Dropdown.OptionData(opt.Key));
}
public static void HelpSelected(int index)
{
if (index == 0)
return;
var helpText = helpDict.ElementAt(index);
Input.Text = helpText.Value;
Panel.HelpDropdown.value = 0;
}
internal const string STARTUP_TEXT = @"<color=#5d8556>// Welcome to the UnityExplorer C# Console!
// It is recommended to use the Log panel (or a console log window) while using this tool.
// Use the Help dropdown to see detailed examples of how to use the console.
// To execute a script automatically on startup, put the script at 'UnityExplorer\Scripts\startup.cs'</color>";
internal const string HELP_USINGS = @"// You can add a using directive to any namespace, but you must compile for it to take effect.
// It will remain in effect until you Reset the console.
using UnityEngine.UI;
// To see your current usings, use the ""GetUsing();"" helper.
// Note: You cannot add usings and evaluate REPL at the same time.";
internal const string HELP_REPL = @"/* REPL (Read-Evaluate-Print-Loop) is a way to execute code immediately.
* REPL code cannot contain any using directives or classes.
* The return value of the last line of your REPL will be printed to the log.
* Variables defined in REPL will exist until you Reset the console.
*/
// eg: This code would print 'Hello, World!', and then print 6 as the return value.
Log(""Hello, world!"");
var x = 5;
++x;
/* The following helpers are available in REPL mode:
* GetUsing(); - prints the current using directives to the console log
* GetVars(); - prints the names and values of the REPL variables you have defined
* GetClasses(); - prints the names and members of the classes you have defined
* Log(obj); - prints a message to the console log
* CurrentTarget; - System.Object, the target of the active Inspector tab
* AllTargets; - System.Object[], the targets of all Inspector tabs
* Inspect(obj); - inspect the object with the Inspector
* Inspect(someType); - inspect a Type with static reflection
* Start(enumerator); - starts the IEnumerator as a Coroutine
* help; - the default REPL help command, contains additional helpers.
*/";
internal const string HELP_CLASSES = @"// Classes you compile will exist until the application closes.
// You can soft-overwrite a class by compiling it again with the same name. The old class will still technically exist in memory.
// Compiled classes can be accessed from both inside and outside this console.
// Note: in IL2CPP, injecting these classes with ClassInjector may crash the game!
public class HelloWorld
{
public static void Main()
{
UnityExplorer.ExplorerCore.Log(""Hello, world!"");
}
}
// In REPL, you could call the example method above with ""HelloWorld.Main();""
// Note: The compiler does not allow you to run REPL code and define classes at the same time.
// In REPL, use the ""GetClasses();"" helper to see the classes you have defined since the last Reset.";
internal const string HELP_COROUTINES = @"// To start a Coroutine directly, use ""Start(SomeCoroutine());"" in REPL mode.
// To declare a coroutine, you will need to compile it separately. For example:
public class MyCoro
{
public static IEnumerator Main()
{
yield return null;
UnityExplorer.ExplorerCore.Log(""Hello, world after one frame!"");
}
}
// To run this Coroutine in REPL, it would look like ""Start(MyCoro.Main());""";
#endregion
}
}

View File

@ -0,0 +1,362 @@
using Mono.CSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityExplorer.CSConsole.Lexers;
namespace UnityExplorer.CSConsole
{
public struct MatchInfo
{
public int startIndex;
public int endIndex;
public bool isStringOrComment;
public bool matchToEndOfLine;
public string htmlColorTag;
}
public class LexerBuilder
{
#region Core and initialization
public const char WHITESPACE = ' ';
public readonly HashSet<char> IndentOpenChars = new HashSet<char> { '{', '(' };
public readonly HashSet<char> IndentCloseChars = new HashSet<char> { '}', ')' };
private readonly Lexer[] lexers;
private readonly HashSet<char> delimiters = new HashSet<char>();
private readonly StringLexer stringLexer = new StringLexer();
private readonly CommentLexer commentLexer = new CommentLexer();
public LexerBuilder()
{
lexers = new Lexer[]
{
commentLexer,
stringLexer,
new SymbolLexer(),
new NumberLexer(),
new KeywordLexer(),
};
foreach (var matcher in lexers)
{
foreach (char c in matcher.Delimiters)
{
if (!delimiters.Contains(c))
delimiters.Add(c);
}
}
}
#endregion
/// <summary>The last committed index for a match or no-match. Starts at -1 for a new parse.</summary>
public int CommittedIndex { get; private set; }
/// <summary>The index of the character we are currently parsing, at minimum it will be CommittedIndex + 1.</summary>
public int CurrentIndex { get; private set; }
/// <summary>The current character we are parsing, determined by CurrentIndex.</summary>
public char Current => !EndOfInput ? currentInput[CurrentIndex] : WHITESPACE;
/// <summary>The previous character (CurrentIndex - 1), or whitespace if no previous character.</summary>
public char Previous => CurrentIndex >= 1 ? currentInput[CurrentIndex - 1] : WHITESPACE;
/// <summary>Returns true if CurrentIndex is >= the current input length.</summary>
public bool EndOfInput => CurrentIndex > currentEndIdx;
/// <summary>Returns true if EndOfInput or current character is a new line.</summary>
public bool EndOrNewLine => EndOfInput || IsNewLine(Current);
public static bool IsNewLine(char c) => c == '\n' || c == '\r';
private string currentInput;
private int currentStartIdx;
private int currentEndIdx;
/// <summary>
/// Parse the range of the string with the Lexer and build a RichText-highlighted representation of it.
/// </summary>
/// <param name="input">The entire input string which you want to parse a section (or all) of</param>
/// <param name="startIdx">The first character you want to highlight</param>
/// <param name="endIdx">The last character you want to highlight</param>
/// <param name="leadingLines">The amount of leading empty lines you want before the first character in the return string.</param>
/// <returns>A string which contains the amount of leading lines specified, as well as the rich-text highlighted section.</returns>
public string BuildHighlightedString(string input, int startIdx, int endIdx, int leadingLines, int caretIdx, out bool caretInStringOrComment)
{
caretInStringOrComment = false;
if (string.IsNullOrEmpty(input) || endIdx <= startIdx)
return input;
currentInput = input;
currentStartIdx = startIdx;
currentEndIdx = endIdx;
var sb = new StringBuilder();
for (int i = 0; i < leadingLines; i++)
sb.Append('\n');
int lastUnhighlighted = startIdx;
foreach (var match in GetMatches())
{
// append non-highlighted text between last match and this
for (int i = lastUnhighlighted; i < match.startIndex; i++)
sb.Append(input[i]);
// append the highlighted match
sb.Append(match.htmlColorTag);
for (int i = match.startIndex; i <= match.endIndex && i <= currentEndIdx; i++)
sb.Append(input[i]);
sb.Append(SignatureHighlighter.CLOSE_COLOR);
// update the last unhighlighted start index
lastUnhighlighted = match.endIndex + 1;
int matchEndIdx = match.endIndex;
if (match.matchToEndOfLine)
{
while (input.Length - 1 >= matchEndIdx)
{
if (IsNewLine(input[matchEndIdx]))
break;
matchEndIdx++;
}
}
// check caretIdx to determine inStringOrComment state
if (caretIdx >= match.startIndex && (caretIdx <= matchEndIdx || (caretIdx >= input.Length && matchEndIdx >= input.Length - 1)))
caretInStringOrComment = match.isStringOrComment;
}
// Append trailing unhighlighted input
while (lastUnhighlighted <= endIdx)
{
sb.Append(input[lastUnhighlighted]);
lastUnhighlighted++;
}
return sb.ToString();
}
// Match builder, iterates through each Lexer and returns all matches found.
public IEnumerable<MatchInfo> GetMatches()
{
CommittedIndex = currentStartIdx - 1;
Rollback();
while (!EndOfInput)
{
SkipWhitespace();
bool anyMatch = false;
int startIndex = CommittedIndex + 1;
foreach (var lexer in lexers)
{
if (lexer.TryMatchCurrent(this))
{
anyMatch = true;
yield return new MatchInfo
{
startIndex = startIndex,
endIndex = CommittedIndex,
htmlColorTag = lexer.ColorTag,
isStringOrComment = lexer is StringLexer || lexer is CommentLexer,
};
break;
}
else
Rollback();
}
if (!anyMatch)
{
CurrentIndex = CommittedIndex + 1;
Commit();
}
}
}
// Methods used by the Lexers for interfacing with the current parse process
public char PeekNext(int amount = 1)
{
CurrentIndex += amount;
return Current;
}
public void Commit()
{
CommittedIndex = Math.Min(currentEndIdx, CurrentIndex);
}
public void Rollback()
{
CurrentIndex = CommittedIndex + 1;
}
public void RollbackBy(int amount)
{
CurrentIndex = Math.Max(CommittedIndex + 1, CurrentIndex - amount);
}
public bool IsDelimiter(char character, bool orWhitespace = false, bool orLetterOrDigit = false)
{
return delimiters.Contains(character)
|| (orWhitespace && char.IsWhiteSpace(character))
|| (orLetterOrDigit && char.IsLetterOrDigit(character));
}
private void SkipWhitespace()
{
// peek and commit as long as there is whitespace
while (!EndOfInput && char.IsWhiteSpace(Current))
{
Commit();
PeekNext();
}
if (!char.IsWhiteSpace(Current))
Rollback();
}
#region Auto Indenting
// Using the Lexer for indenting as it already has what we need to tokenize strings and comments.
// At the moment this only handles when a single newline or close-delimiter is composed.
// Does not handle copy+paste or any other characters yet.
public string IndentCharacter(string input, ref int caretIndex)
{
int lastCharIndex = caretIndex - 1;
char c = input[lastCharIndex];
// we only want to indent for new lines and close indents
if (!IsNewLine(c) && !IndentCloseChars.Contains(c))
return input;
// perform a light parse up to the caret to determine indent level
currentInput = input;
currentStartIdx = 0;
currentEndIdx = lastCharIndex;
CommittedIndex = -1;
Rollback();
int indent = 0;
while (!EndOfInput)
{
if (CurrentIndex >= lastCharIndex)
{
// reached the caret index
if (indent <= 0)
break;
if (IsNewLine(c))
input = IndentNewLine(input, indent, ref caretIndex);
else // closing indent
input = IndentCloseDelimiter(input, indent, lastCharIndex, ref caretIndex);
break;
}
// Try match strings and comments (Lexer will commit to the end of the match)
if (stringLexer.TryMatchCurrent(this) || commentLexer.TryMatchCurrent(this))
{
PeekNext();
continue;
}
// Still parsing, check indent
if (IndentOpenChars.Contains(Current))
indent++;
else if (IndentCloseChars.Contains(Current))
indent--;
Commit();
PeekNext();
}
return input;
}
private string IndentNewLine(string input, int indent, ref int caretIndex)
{
// continue until the end of line or next non-whitespace character.
// if there's a close-indent on this line, reduce the indent level.
while (CurrentIndex < input.Length - 1)
{
CurrentIndex++;
char next = input[CurrentIndex];
if (IsNewLine(next))
break;
if (char.IsWhiteSpace(next))
continue;
else if (IndentCloseChars.Contains(next))
indent--;
break;
}
if (indent > 0)
{
input = input.Insert(caretIndex, new string('\t', indent));
caretIndex += indent;
}
return input;
}
private string IndentCloseDelimiter(string input, int indent, int lastCharIndex, ref int caretIndex)
{
if (CurrentIndex > lastCharIndex)
{
return input;
}
// lower the indent level by one as we would not have accounted for this closing symbol
indent--;
// go back from the caret to the start of the line, calculate how much indent we need to adjust.
while (CurrentIndex > 0)
{
CurrentIndex--;
char prev = input[CurrentIndex];
if (IsNewLine(prev))
break;
if (!char.IsWhiteSpace(prev))
{
// the line containing the closing bracket has non-whitespace characters before it. do not indent.
indent = 0;
break;
}
else if (prev == '\t')
indent--;
}
if (indent > 0)
{
input = input.Insert(caretIndex, new string('\t', indent));
caretIndex += indent;
}
else if (indent < 0)
{
// line is overly indented
input = input.Remove(lastCharIndex - 1, -indent);
caretIndex += indent;
}
return input;
}
#endregion
}
}

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexers
{
public class CommentLexer : Lexer
{
private enum CommentType
{
Line,
Block
}
// forest green
protected override Color HighlightColor => new Color(0.34f, 0.65f, 0.29f, 1.0f);
public override bool TryMatchCurrent(LexerBuilder lexer)
{
if (lexer.Current == '/')
{
lexer.PeekNext();
if (lexer.Current == '/')
{
// line comment. read to end of line or file.
do
{
lexer.Commit();
lexer.PeekNext();
}
while (!lexer.EndOrNewLine);
return true;
}
else if (lexer.Current == '*')
{
// block comment, read until end of file or closing '*/'
lexer.PeekNext();
do
{
lexer.PeekNext();
lexer.Commit();
}
while (!lexer.EndOfInput && !(lexer.Current == '/' && lexer.Previous == '*'));
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexers
{
public class KeywordLexer : Lexer
{
// system blue
protected override Color HighlightColor => new Color(0.33f, 0.61f, 0.83f, 1.0f);
public static readonly HashSet<string> keywords = new HashSet<string>
{
// reserved keywords
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue",
"decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally",
"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock",
"long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public",
"readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch",
"this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "void",
"volatile", "while",
// contextual keywords
"add", "and", "alias", "ascending", "async", "await", "by", "descending", "dynamic", "equals", "from", "get",
"global", "group", "init", "into", "join", "let", "managed", "nameof", "not", "notnull", "on",
"or", "orderby", "partial", "record", "remove", "select", "set", "unmanaged", "value", "var", "when", "where",
"where", "with", "yield", "nint", "nuint"
};
public override bool TryMatchCurrent(LexerBuilder lexer)
{
var prev = lexer.Previous;
var first = lexer.Current;
// check for keywords
if (lexer.IsDelimiter(prev, true) && char.IsLetter(first))
{
// can be a keyword...
var sb = new StringBuilder();
sb.Append(lexer.Current);
while (!lexer.EndOfInput && char.IsLetter(lexer.PeekNext()))
sb.Append(lexer.Current);
// next must be whitespace or delimiter
if (!lexer.EndOfInput && !(char.IsWhiteSpace(lexer.Current) || lexer.IsDelimiter(lexer.Current)))
return false;
if (keywords.Contains(sb.ToString()))
{
if (!lexer.EndOfInput)
lexer.RollbackBy(1);
lexer.Commit();
return true;
}
return false;
}
else
return false;
}
}
}

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexers
{
public abstract class Lexer
{
public virtual IEnumerable<char> Delimiters => Enumerable.Empty<char>();
protected abstract Color HighlightColor { get; }
public string ColorTag => colorTag ?? (colorTag = "<color=#" + HighlightColor.ToHex() + ">");
private string colorTag;
public abstract bool TryMatchCurrent(LexerBuilder lexer);
}
}

View File

@ -0,0 +1,32 @@
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexers
{
public class NumberLexer : Lexer
{
// Maroon
protected override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f);
private bool IsNumeric(char c) => char.IsNumber(c) || c == '.';
public override bool TryMatchCurrent(LexerBuilder lexer)
{
// previous character must be whitespace or delimiter
if (!lexer.IsDelimiter(lexer.Previous, true))
return false;
if (!IsNumeric(lexer.Current))
return false;
while (!lexer.EndOfInput)
{
lexer.Commit();
if (!IsNumeric(lexer.PeekNext()))
break;
}
return true;
}
}
}

View File

@ -0,0 +1,80 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexers
{
public class StringLexer : Lexer
{
public override IEnumerable<char> Delimiters => new[] { '"', '\'', };
// orange
protected override Color HighlightColor => new Color(0.79f, 0.52f, 0.32f, 1.0f);
public override bool TryMatchCurrent(LexerBuilder lexer)
{
if (lexer.Current == '"')
{
if (lexer.Previous == '@')
{
// verbatim string, continue until un-escaped quote.
while (!lexer.EndOfInput)
{
lexer.Commit();
if (lexer.PeekNext() == '"')
{
lexer.Commit();
// possibly the end, check for escaped quotes.
// commit the character and flip the escape bool for each quote.
bool escaped = false;
while (lexer.PeekNext() == '"')
{
lexer.Commit();
escaped = !escaped;
}
// if the last quote wasnt escaped, that was the end of the string.
if (!escaped)
break;
}
}
}
else
{
// normal string
// continue until a quote which is not escaped, or end of input
while (!lexer.EndOfInput)
{
lexer.Commit();
lexer.PeekNext();
if ((lexer.Current == '"') && lexer.Previous != '\\')
{
lexer.Commit();
break;
}
}
}
return true;
}
else if (lexer.Current == '\'')
{
// char
while (!lexer.EndOfInput)
{
lexer.Commit();
lexer.PeekNext();
if ((lexer.Current == '\'') && lexer.Previous != '\\')
{
lexer.Commit();
break;
}
}
return true;
}
else
return false;
}
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexers
{
public class SymbolLexer : Lexer
{
// silver
protected override Color HighlightColor => new Color(0.6f, 0.6f, 0.6f);
// all symbols are delimiters
public override IEnumerable<char> Delimiters => symbols.Where(it => it != '.'); // '.' is not a delimiter, only a separator.
public static bool IsSymbol(char c) => symbols.Contains(c);
public static readonly HashSet<char> symbols = new HashSet<char>
{
'[', '{', '(', // open
']', '}', ')', // close
'.', ',', ';', ':', '?', '@', // special
// operators
'+', '-', '*', '/', '%', '&', '|', '^', '~', '=', '<', '>', '!',
};
public override bool TryMatchCurrent(LexerBuilder lexer)
{
// previous character must be delimiter, whitespace, or alphanumeric.
if (!lexer.IsDelimiter(lexer.Previous, true, true))
return false;
if (IsSymbol(lexer.Current))
{
do
{
lexer.Commit();
lexer.PeekNext();
}
while (IsSymbol(lexer.Current));
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,83 @@
using Mono.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
// Thanks to ManlyMarco for this
namespace UnityExplorer.CSConsole
{
public class ScriptEvaluator : Evaluator, IDisposable
{
private static readonly HashSet<string> StdLib = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"mscorlib", "System.Core", "System", "System.Xml"
};
internal static TextWriter _textWriter;
internal static StreamReportPrinter _reportPrinter;
public ScriptEvaluator(TextWriter tw) : base(BuildContext(tw))
{
_textWriter = tw;
ImportAppdomainAssemblies(Reference);
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
}
public void Dispose()
{
AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad;
_textWriter.Dispose();
}
private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
string name = args.LoadedAssembly.GetName().Name;
if (StdLib.Contains(name))
return;
Reference(args.LoadedAssembly);
}
private void Reference(Assembly asm)
{
var name = asm.GetName().Name;
if (name == "completions")
return;
ReferenceAssembly(asm);
}
private static CompilerContext BuildContext(TextWriter tw)
{
_reportPrinter = new StreamReportPrinter(tw);
var settings = new CompilerSettings
{
Version = LanguageVersion.Experimental,
GenerateDebugInfo = false,
StdLib = true,
Target = Target.Library,
WarningLevel = 0,
EnhancedWarnings = false
};
return new CompilerContext(settings, _reportPrinter);
}
private static void ImportAppdomainAssemblies(Action<Assembly> import)
{
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
string name = assembly.GetName().Name;
if (StdLib.Contains(name))
continue;
import(assembly);
}
}
}
}

View File

@ -0,0 +1,79 @@
using Mono.CSharp;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityExplorer.Core.Runtime;
/*
Welcome to the UnityExplorer C# Console!
Use the Help dropdown to see detailed examples of how to use this console.
To see your output, use the Log panel or a Console Log window.
*/
namespace UnityExplorer.CSConsole
{
public class ScriptInteraction : InteractiveBase
{
public static void Log(object message)
{
ExplorerCore.Log(message);
}
public static object CurrentTarget => InspectorManager.ActiveInspector?.Target;
public static object[] AllTargets => InspectorManager.Inspectors.Select(it => it.Target).ToArray();
public static void Inspect(object obj)
{
InspectorManager.Inspect(obj);
}
public static void Inspect(Type type)
{
InspectorManager.Inspect(type);
}
public static void Start(IEnumerator ienumerator)
{
RuntimeProvider.Instance.StartCoroutine(ienumerator);
}
public static void GetUsing()
{
Log(Evaluator.GetUsing());
}
public static void GetVars()
{
var vars = Evaluator.GetVars()?.Trim();
if (string.IsNullOrEmpty(vars))
ExplorerCore.LogWarning("No variables seem to be defined!");
else
Log(vars);
}
public static void GetClasses()
{
if (ReflectionUtility.GetFieldInfo(typeof(Evaluator), "source_file")
.GetValue(Evaluator) is CompilationSourceFile sourceFile
&& sourceFile.Containers.Any())
{
var sb = new StringBuilder();
sb.Append($"There are {sourceFile.Containers.Count} defined classes:");
foreach (TypeDefinition type in sourceFile.Containers.Where(it => it is TypeDefinition))
{
sb.Append($"\n\n{type.MemberName.Name}:");
foreach (var member in type.Members)
sb.Append($"\n\t- {member.AttributeTargets}: \"{member.MemberName.Name}\" ({member.ModFlags})");
}
Log(sb.ToString());
}
else
ExplorerCore.LogWarning("No classes seem to be defined.");
}
}
}