fix Autocomplete buttons moving when you click them, rename Console namespace to CSConsole

This commit is contained in:
sinaioutlander
2020-11-13 23:50:24 +11:00
parent 7a4c7eb498
commit e9acd68ee4
15 changed files with 37 additions and 32 deletions

View File

@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.Helpers;
using UnityExplorer.UI;
using UnityExplorer.UI.Modules;
namespace UnityExplorer.CSConsole
{
public class AutoCompleter
{
public static AutoCompleter Instance;
public const int MAX_LABELS = 500;
private const int UPDATES_PER_BATCH = 100;
public static GameObject m_mainObj;
//private static RectTransform m_thisRect;
private static readonly List<GameObject> m_suggestionButtons = new List<GameObject>();
private static readonly List<Text> m_suggestionTexts = new List<Text>();
private static readonly List<Text> m_hiddenSuggestionTexts = new List<Text>();
private static bool m_suggestionsDirty;
private static Suggestion[] m_suggestions = new Suggestion[0];
private static int m_lastBatchIndex;
private static string m_prevInput = "NULL";
private static int m_lastCaretPos;
public static void Init()
{
ConstructUI();
m_mainObj.SetActive(false);
}
public static void Update()
{
if (!m_mainObj)
{
return;
}
if (!CodeEditor.EnableAutocompletes)
{
if (m_mainObj.activeSelf)
{
m_mainObj.SetActive(false);
}
return;
}
RefreshButtons();
UpdatePosition();
}
public static void SetSuggestions(Suggestion[] suggestions)
{
m_suggestions = suggestions;
m_suggestionsDirty = true;
m_lastBatchIndex = 0;
}
private static void RefreshButtons()
{
if (!m_suggestionsDirty)
{
return;
}
if (m_suggestions.Length < 1)
{
if (m_mainObj.activeSelf)
{
m_mainObj?.SetActive(false);
}
return;
}
if (!m_mainObj.activeSelf)
{
m_mainObj.SetActive(true);
}
if (m_suggestions.Length < 1 || m_lastBatchIndex >= MAX_LABELS)
{
m_suggestionsDirty = false;
return;
}
int end = m_lastBatchIndex + UPDATES_PER_BATCH;
for (int i = m_lastBatchIndex; i < end && i < MAX_LABELS; i++)
{
if (i >= m_suggestions.Length)
{
if (m_suggestionButtons[i].activeSelf)
{
m_suggestionButtons[i].SetActive(false);
}
}
else
{
if (!m_suggestionButtons[i].activeSelf)
{
m_suggestionButtons[i].SetActive(true);
}
var suggestion = m_suggestions[i];
var label = m_suggestionTexts[i];
var hiddenLabel = m_hiddenSuggestionTexts[i];
label.text = suggestion.Full;
hiddenLabel.text = suggestion.Addition;
label.color = suggestion.TextColor;
}
m_lastBatchIndex = i;
}
m_lastBatchIndex++;
}
private static void UpdatePosition()
{
try
{
var editor = ConsolePage.Instance.m_codeEditor;
if (!editor.InputField.isFocused)
return;
var textGen = editor.InputText.cachedTextGenerator;
int caretPos = editor.m_lastCaretPos;
if (caretPos == m_lastCaretPos)
return;
m_lastCaretPos = caretPos;
if (caretPos >= 1)
caretPos--;
var pos = textGen.characters[caretPos].cursorPos;
pos = editor.InputField.transform.TransformPoint(pos);
m_mainObj.transform.position = new Vector3(pos.x + 10, pos.y - 20, 0);
}
catch //(Exception e)
{
//ExplorerCore.Log(e.ToString());
}
}
private static readonly char[] splitChars = new[] { '{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?' };
public static void CheckAutocomplete()
{
var m_codeEditor = ConsolePage.Instance.m_codeEditor;
string input = m_codeEditor.InputField.text;
int caretIndex = m_codeEditor.InputField.caretPosition;
if (!string.IsNullOrEmpty(input))
{
try
{
int start = caretIndex <= 0 ? 0 : input.LastIndexOfAny(splitChars, caretIndex - 1) + 1;
input = input.Substring(start, caretIndex - start).Trim();
}
catch (ArgumentException) { }
}
if (!string.IsNullOrEmpty(input) && input != m_prevInput)
{
GetAutocompletes(input);
}
else
{
ClearAutocompletes();
}
m_prevInput = input;
}
public static void ClearAutocompletes()
{
if (CodeEditor.AutoCompletes.Any())
{
CodeEditor.AutoCompletes.Clear();
}
}
public static void GetAutocompletes(string input)
{
try
{
// Credit ManylMarco
CodeEditor.AutoCompletes.Clear();
string[] completions = ConsolePage.Instance.m_evaluator.GetCompletions(input, out string prefix);
if (completions != null)
{
if (prefix == null)
{
prefix = input;
}
CodeEditor.AutoCompletes.AddRange(completions
.Where(x => !string.IsNullOrEmpty(x))
.Select(x => new Suggestion(x, prefix, Suggestion.Contexts.Other))
);
}
string trimmed = input.Trim();
if (trimmed.StartsWith("using"))
{
trimmed = trimmed.Remove(0, 5).Trim();
}
IEnumerable<Suggestion> namespaces = Suggestion.Namespaces
.Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length)
.Select(x => new Suggestion(
x.Substring(trimmed.Length),
x.Substring(0, trimmed.Length),
Suggestion.Contexts.Namespace));
CodeEditor.AutoCompletes.AddRange(namespaces);
IEnumerable<Suggestion> keywords = Suggestion.Keywords
.Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length)
.Select(x => new Suggestion(
x.Substring(trimmed.Length),
x.Substring(0, trimmed.Length),
Suggestion.Contexts.Keyword));
CodeEditor.AutoCompletes.AddRange(keywords);
}
catch (Exception ex)
{
ExplorerCore.Log("Autocomplete error:\r\n" + ex.ToString());
ClearAutocompletes();
}
}
#region UI Construction
private static void ConstructUI()
{
var parent = UIManager.CanvasRoot;
var obj = UIFactory.CreateScrollView(parent, out GameObject content, out _, new Color(0.1f, 0.1f, 0.1f, 0.95f));
m_mainObj = obj;
var mainRect = obj.GetComponent<RectTransform>();
//m_thisRect = mainRect;
mainRect.pivot = new Vector2(0f, 1f);
mainRect.anchorMin = new Vector2(0.45f, 0.45f);
mainRect.anchorMax = new Vector2(0.65f, 0.6f);
mainRect.offsetMin = Vector2.zero;
mainRect.offsetMax = Vector2.zero;
var mainGroup = content.GetComponent<VerticalLayoutGroup>();
mainGroup.childControlHeight = false;
mainGroup.childControlWidth = true;
mainGroup.childForceExpandHeight = false;
mainGroup.childForceExpandWidth = true;
for (int i = 0; i < MAX_LABELS; i++)
{
var buttonObj = UIFactory.CreateButton(content);
Button btn = buttonObj.GetComponent<Button>();
ColorBlock btnColors = btn.colors;
btnColors.normalColor = new Color(0f, 0f, 0f, 0f);
btnColors.highlightedColor = new Color(0.2f, 0.2f, 0.2f, 1.0f);
btn.colors = btnColors;
var nav = btn.navigation;
nav.mode = Navigation.Mode.Vertical;
btn.navigation = nav;
var btnLayout = buttonObj.AddComponent<LayoutElement>();
btnLayout.minHeight = 20;
var text = btn.GetComponentInChildren<Text>();
text.alignment = TextAnchor.MiddleLeft;
text.color = Color.white;
var hiddenChild = UIFactory.CreateUIObject("HiddenText", buttonObj);
hiddenChild.SetActive(false);
var hiddenText = hiddenChild.AddComponent<Text>();
m_hiddenSuggestionTexts.Add(hiddenText);
btn.onClick.AddListener(UseAutocompleteButton);
void UseAutocompleteButton()
{
ConsolePage.Instance.m_codeEditor.UseAutocomplete(hiddenText.text);
}
m_suggestionButtons.Add(buttonObj);
m_suggestionTexts.Add(text);
}
}
#endregion
}
}

View File

@ -0,0 +1,308 @@
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityExplorer.CSConsole.Lexer;
namespace UnityExplorer.CSConsole
{
public struct LexerMatchInfo
{
public int startIndex;
public int endIndex;
public string htmlColor;
}
public enum DelimiterType
{
Start,
End,
};
public class CSharpLexer
{
private string inputString;
private readonly Matcher[] matchers;
private readonly HashSet<char> startDelimiters;
private readonly HashSet<char> endDelimiters;
private int currentIndex;
private int currentLookaheadIndex;
public char Current { get; private set; }
public char Previous { get; private set; }
public bool EndOfStream => currentLookaheadIndex >= inputString.Length;
public static char indentOpen = '{';
public static char indentClose = '}';
private static readonly StringBuilder indentBuilder = new StringBuilder();
public static char[] delimiters = new[]
{
'[', ']', '(', ')', '{', '}', ';', ':', ',', '.'
};
public static CommentMatch commentMatcher = new CommentMatch();
public static SymbolMatch symbolMatcher = new SymbolMatch();
public static NumberMatch numberMatcher = new NumberMatch();
public static StringMatch stringMatcher = new StringMatch();
public static KeywordMatch validKeywordMatcher = new KeywordMatch
{
highlightColor = new Color(0.33f, 0.61f, 0.83f, 1.0f),
Keywords = new[] { "add", "as", "ascending", "await", "bool", "break", "by", "byte",
"case", "catch", "char", "checked", "const", "continue", "decimal", "default", "descending", "do", "dynamic",
"else", "equals", "false", "finally", "float", "for", "foreach", "from", "global", "goto", "group",
"if", "in", "int", "into", "is", "join", "let", "lock", "long", "new", "null", "object", "on", "orderby", "out",
"ref", "remove", "return", "sbyte", "select", "short", "sizeof", "stackalloc", "string",
"switch", "throw", "true", "try", "typeof", "uint", "ulong", "ushort", "var", "where", "while", "yield" }
};
public static KeywordMatch invalidKeywordMatcher = new KeywordMatch()
{
highlightColor = new Color(0.95f, 0.10f, 0.10f, 1.0f),
Keywords = new[] { "abstract", "async", "base", "class", "delegate", "enum", "explicit", "extern", "fixed", "get",
"implicit", "interface", "internal", "namespace", "operator", "override", "params", "private", "protected", "public",
"using", "partial", "readonly", "sealed", "set", "static", "struct", "this", "unchecked", "unsafe", "value", "virtual", "volatile", "void" }
};
// ~~~~~~~ ctor ~~~~~~~
public CSharpLexer()
{
startDelimiters = new HashSet<char>(delimiters);
endDelimiters = new HashSet<char>(delimiters);
this.matchers = new Matcher[]
{
commentMatcher,
symbolMatcher,
numberMatcher,
stringMatcher,
validKeywordMatcher,
invalidKeywordMatcher,
};
foreach (Matcher lexer in matchers)
{
foreach (char c in lexer.StartChars)
{
if (!startDelimiters.Contains(c))
startDelimiters.Add(c);
}
foreach (char c in lexer.EndChars)
{
if (!endDelimiters.Contains(c))
endDelimiters.Add(c);
}
}
}
// ~~~~~~~ Lex Matching ~~~~~~~
public IEnumerable<LexerMatchInfo> GetMatches(string input)
{
if (input == null || matchers == null || matchers.Length == 0)
{
yield break;
}
inputString = input;
Current = ' ';
Previous = ' ';
currentIndex = 0;
currentLookaheadIndex = 0;
while (!EndOfStream)
{
bool didMatchLexer = false;
ReadWhiteSpace();
foreach (Matcher matcher in matchers)
{
int startIndex = currentIndex;
bool isMatched = matcher.IsMatch(this);
if (isMatched)
{
int endIndex = currentIndex;
didMatchLexer = true;
yield return new LexerMatchInfo
{
startIndex = startIndex,
endIndex = endIndex,
htmlColor = matcher.HexColor,
};
break;
}
}
if (!didMatchLexer)
{
ReadNext();
Commit();
}
}
}
// ~~~~~~~ Indent ~~~~~~~
public static string GetIndentForInput(string input, int indent, out int caretPosition)
{
indentBuilder.Clear();
indent += 1;
bool stringState = false;
for (int i = 0; i < input.Length; i++)
{
if (input[i] == '"')
{
stringState = !stringState;
}
if (input[i] == '\n')
{
indentBuilder.Append('\n');
for (int j = 0; j < indent; j++)
{
indentBuilder.Append("\t");
}
}
else if (input[i] == '\t')
{
continue;
}
else if (!stringState && input[i] == indentOpen)
{
indentBuilder.Append(indentOpen);
indent++;
}
else if (!stringState && input[i] == indentClose)
{
indentBuilder.Append(indentClose);
indent--;
}
else
{
indentBuilder.Append(input[i]);
}
}
string formattedSection = indentBuilder.ToString();
caretPosition = formattedSection.Length - 1;
for (int i = formattedSection.Length - 1; i >= 0; i--)
{
if (formattedSection[i] == '\n')
{
continue;
}
caretPosition = i;
break;
}
return formattedSection;
}
public static int GetIndentLevel(string inputString, int startIndex, int endIndex)
{
int indent = 0;
for (int i = startIndex; i < endIndex; i++)
{
if (inputString[i] == '\t')
{
indent++;
}
// Check for end line or other characters
if (inputString[i] == '\n' || inputString[i] != ' ')
{
break;
}
}
return indent;
}
// Lexer reading
public char ReadNext()
{
if (EndOfStream)
{
return '\0';
}
Previous = Current;
Current = inputString[currentLookaheadIndex];
currentLookaheadIndex++;
return Current;
}
public void Rollback(int amount = -1)
{
if (amount == -1)
{
currentLookaheadIndex = currentIndex;
}
else
{
if (currentLookaheadIndex > currentIndex)
{
currentLookaheadIndex -= amount;
}
}
int previousIndex = currentLookaheadIndex - 1;
if (previousIndex >= inputString.Length)
{
Previous = inputString[inputString.Length - 1];
}
else if (previousIndex >= 0)
{
Previous = inputString[previousIndex];
}
else
{
Previous = ' ';
}
}
public void Commit()
{
currentIndex = currentLookaheadIndex;
}
public bool IsSpecialSymbol(char character, DelimiterType position = DelimiterType.Start)
{
if (position == DelimiterType.Start)
{
return startDelimiters.Contains(character);
}
return endDelimiters.Contains(character);
}
private void ReadWhiteSpace()
{
while (char.IsWhiteSpace(ReadNext()) == true)
{
Commit();
}
Rollback();
}
}
}

481
src/CSConsole/CodeEditor.cs Normal file
View File

@ -0,0 +1,481 @@
using System;
using System.Linq;
using System.Text;
using UnityExplorer.Input;
using UnityExplorer.CSConsole.Lexer;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.UI;
using UnityExplorer.UI.Modules;
using System.Collections.Generic;
using System.Reflection;
using UnityExplorer.UI.Shared;
using UnityExplorer.Helpers;
namespace UnityExplorer.CSConsole
{
// Handles most of the UI side of the C# console, including syntax highlighting.
public class CodeEditor
{
public InputField InputField { get; internal set; }
public Text InputText { get; internal set; }
public int CurrentIndent { get; private set; }
public static bool EnableCtrlRShortcut { get; set; } = true;
public static bool EnableAutoIndent { get; set; } = true;
public static bool EnableAutocompletes { get; set; } = true;
public static List<Suggestion> AutoCompletes = new List<Suggestion>();
public string HighlightedText => inputHighlightText.text;
private Text inputHighlightText;
private readonly CSharpLexer highlightLexer;
private readonly StringBuilder sbHighlight;
internal int m_lastCaretPos;
internal int m_fixCaretPos;
internal bool m_fixwanted;
internal float m_lastSelectAlpha;
private static readonly KeyCode[] onFocusKeys =
{
KeyCode.Return, KeyCode.Backspace, KeyCode.UpArrow,
KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow
};
internal const string STARTUP_TEXT = @"Welcome to the UnityExplorer C# Console.
The following helper methods are available:
* <color=#add490>Log(""message"")</color> logs a message to the debug console
* <color=#add490>CurrentTarget()</color> returns the currently inspected target on the Home page
* <color=#add490>AllTargets()</color> returns an object[] array containing all inspected instances
* <color=#add490>Inspect(someObject)</color> to inspect an instance, eg. Inspect(Camera.main);
* <color=#add490>Inspect(typeof(SomeClass))</color> to inspect a Class with static reflection
* <color=#add490>AddUsing(""SomeNamespace"")</color> adds a using directive to the C# console
* <color=#add490>GetUsing()</color> logs the current using directives to the debug console
* <color=#add490>Reset()</color> resets all using directives and variables
";
public CodeEditor()
{
sbHighlight = new StringBuilder();
highlightLexer = new CSharpLexer();
ConstructUI();
InputField.onValueChanged.AddListener((string s) => { OnInputChanged(s); });
}
public void Update()
{
if (EnableCtrlRShortcut)
{
if ((InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl))
&& InputManager.GetKeyDown(KeyCode.R))
{
var text = InputField.text.Trim();
if (!string.IsNullOrEmpty(text))
{
ConsolePage.Instance.Evaluate(text);
return;
}
}
}
if (EnableAutoIndent && InputManager.GetKeyDown(KeyCode.Return))
AutoIndentCaret();
if (EnableAutocompletes && InputField.isFocused)
{
if (InputManager.GetMouseButton(0) || onFocusKeys.Any(it => InputManager.GetKeyDown(it)))
UpdateAutocompletes();
}
if (m_fixCaretPos > 0)
{
if (!m_fixwanted)
{
EventSystem.current.SetSelectedGameObject(ConsolePage.Instance.m_codeEditor.InputField.gameObject, null);
m_fixwanted = true;
}
else
{
InputField.caretPosition = m_fixCaretPos;
InputField.selectionFocusPosition = m_fixCaretPos;
m_fixwanted = false;
m_fixCaretPos = -1;
var color = InputField.selectionColor;
color.a = m_lastSelectAlpha;
InputField.selectionColor = color;
}
}
else if (InputField.caretPosition > 0)
{
m_lastCaretPos = InputField.caretPosition;
}
}
internal void UpdateAutocompletes()
{
AutoCompleter.CheckAutocomplete();
AutoCompleter.SetSuggestions(AutoCompletes.ToArray());
}
public void UseAutocomplete(string suggestion)
{
string input = InputField.text;
input = input.Insert(m_lastCaretPos, suggestion);
InputField.text = input;
m_fixCaretPos = m_lastCaretPos += suggestion.Length;
var color = InputField.selectionColor;
m_lastSelectAlpha = color.a;
color.a = 0f;
InputField.selectionColor = color;
AutoCompleter.ClearAutocompletes();
}
public void OnInputChanged(string newInput, bool forceUpdate = false)
{
string newText = newInput;
UpdateIndent(newInput);
if (!forceUpdate && string.IsNullOrEmpty(newText))
inputHighlightText.text = string.Empty;
else
inputHighlightText.text = SyntaxHighlightContent(newText);
UpdateAutocompletes();
}
private void UpdateIndent(string newText)
{
int caret = InputField.caretPosition;
int len = newText.Length;
if (caret < 0 || caret >= len)
{
while (caret >= 0 && caret >= len)
caret--;
if (caret < 0)
return;
}
CurrentIndent = 0;
bool stringState = false;
for (int i = 0; i < caret && i < newText.Length; i++)
{
char character = newText[i];
if (character == '"')
stringState = !stringState;
else if (!stringState && character == CSharpLexer.indentOpen)
CurrentIndent++;
else if (!stringState && character == CSharpLexer.indentClose)
CurrentIndent--;
}
if (CurrentIndent < 0)
CurrentIndent = 0;
}
private const string CLOSE_COLOR_TAG = "</color>";
private string SyntaxHighlightContent(string inputText)
{
int offset = 0;
sbHighlight.Length = 0;
foreach (LexerMatchInfo match in highlightLexer.GetMatches(inputText))
{
for (int i = offset; i < match.startIndex; i++)
{
sbHighlight.Append(inputText[i]);
}
sbHighlight.Append($"{match.htmlColor}");
for (int i = match.startIndex; i < match.endIndex; i++)
{
sbHighlight.Append(inputText[i]);
}
sbHighlight.Append(CLOSE_COLOR_TAG);
offset = match.endIndex;
}
for (int i = offset; i < inputText.Length; i++)
{
sbHighlight.Append(inputText[i]);
}
inputText = sbHighlight.ToString();
return inputText;
}
private void AutoIndentCaret()
{
if (CurrentIndent > 0)
{
string indent = GetAutoIndentTab(CurrentIndent);
if (indent.Length > 0)
{
int caretPos = InputField.caretPosition;
string indentMinusOne = indent.Substring(0, indent.Length - 1);
// get last index of {
// chuck it on the next line if its not already
string text = InputField.text;
string sub = InputField.text.Substring(0, InputField.caretPosition);
int lastIndex = sub.LastIndexOf("{");
int offset = lastIndex - 1;
if (offset >= 0 && text[offset] != '\n' && text[offset] != '\t')
{
string open = "\n" + indentMinusOne;
InputField.text = text.Insert(offset + 1, open);
caretPos += open.Length;
}
// check if should add auto-close }
int numOpen = InputField.text.Where(x => x == CSharpLexer.indentOpen).Count();
int numClose = InputField.text.Where(x => x == CSharpLexer.indentClose).Count();
if (numOpen > numClose)
{
// add auto-indent closing
indentMinusOne = $"\n{indentMinusOne}}}";
InputField.text = InputField.text.Insert(caretPos, indentMinusOne);
}
// insert the actual auto indent now
InputField.text = InputField.text.Insert(caretPos, indent);
//InputField.stringPosition = caretPos + indent.Length;
InputField.caretPosition = caretPos + indent.Length;
}
}
// Update line column and indent positions
UpdateIndent(InputField.text);
InputText.text = InputField.text;
//inputText.SetText(InputField.text, true);
InputText.Rebuild(CanvasUpdate.Prelayout);
InputField.ForceLabelUpdate();
InputField.Rebuild(CanvasUpdate.Prelayout);
OnInputChanged(InputText.text, true);
}
private string GetAutoIndentTab(int amount)
{
string tab = string.Empty;
for (int i = 0; i < amount; i++)
{
tab += "\t";
}
return tab;
}
// ========== UI CONSTRUCTION =========== //
public void ConstructUI()
{
ConsolePage.Instance.Content = UIFactory.CreateUIObject("C# Console", MainMenu.Instance.PageViewport);
var mainLayout = ConsolePage.Instance.Content.AddComponent<LayoutElement>();
mainLayout.preferredHeight = 9900;
mainLayout.flexibleHeight = 9000;
var mainGroup = ConsolePage.Instance.Content.AddComponent<VerticalLayoutGroup>();
mainGroup.childControlHeight = true;
mainGroup.childControlWidth = true;
mainGroup.childForceExpandHeight = true;
mainGroup.childForceExpandWidth = true;
#region TOP BAR
// Main group object
var topBarObj = UIFactory.CreateHorizontalGroup(ConsolePage.Instance.Content);
LayoutElement topBarLayout = topBarObj.AddComponent<LayoutElement>();
topBarLayout.minHeight = 50;
topBarLayout.flexibleHeight = 0;
var topBarGroup = topBarObj.GetComponent<HorizontalLayoutGroup>();
topBarGroup.padding.left = 30;
topBarGroup.padding.right = 30;
topBarGroup.padding.top = 8;
topBarGroup.padding.bottom = 8;
topBarGroup.spacing = 10;
topBarGroup.childForceExpandHeight = true;
topBarGroup.childForceExpandWidth = true;
topBarGroup.childControlWidth = true;
topBarGroup.childControlHeight = true;
topBarGroup.childAlignment = TextAnchor.LowerCenter;
var topBarLabel = UIFactory.CreateLabel(topBarObj, TextAnchor.MiddleLeft);
var topBarLabelLayout = topBarLabel.AddComponent<LayoutElement>();
topBarLabelLayout.preferredWidth = 150;
topBarLabelLayout.flexibleWidth = 5000;
var topBarText = topBarLabel.GetComponent<Text>();
topBarText.text = "C# Console";
topBarText.fontSize = 20;
// Enable Ctrl+R toggle
var ctrlRToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle ctrlRToggle, out Text ctrlRToggleText);
ctrlRToggle.onValueChanged.AddListener(CtrlRToggleCallback);
void CtrlRToggleCallback(bool val)
{
EnableCtrlRShortcut = val;
}
ctrlRToggleText.text = "Run on Ctrl+R";
ctrlRToggleText.alignment = TextAnchor.UpperLeft;
var ctrlRLayout = ctrlRToggleObj.AddComponent<LayoutElement>();
ctrlRLayout.minWidth = 140;
ctrlRLayout.flexibleWidth = 0;
ctrlRLayout.minHeight = 25;
// Enable Suggestions toggle
var suggestToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle suggestToggle, out Text suggestToggleText);
suggestToggle.onValueChanged.AddListener(SuggestToggleCallback);
void SuggestToggleCallback(bool val)
{
EnableAutocompletes = val;
AutoCompleter.Update();
}
suggestToggleText.text = "Suggestions";
suggestToggleText.alignment = TextAnchor.UpperLeft;
var suggestLayout = suggestToggleObj.AddComponent<LayoutElement>();
suggestLayout.minWidth = 120;
suggestLayout.flexibleWidth = 0;
suggestLayout.minHeight = 25;
// Enable Auto-indent toggle
var autoIndentToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle autoIndentToggle, out Text autoIndentToggleText);
autoIndentToggle.onValueChanged.AddListener(OnIndentChanged);
void OnIndentChanged(bool val) => EnableAutoIndent = val;
autoIndentToggleText.text = "Auto-indent";
autoIndentToggleText.alignment = TextAnchor.UpperLeft;
var autoIndentLayout = autoIndentToggleObj.AddComponent<LayoutElement>();
autoIndentLayout.minWidth = 120;
autoIndentLayout.flexibleWidth = 0;
autoIndentLayout.minHeight = 25;
#endregion
#region CONSOLE INPUT
int fontSize = 16;
var inputObj = UIFactory.CreateSrollInputField(ConsolePage.Instance.Content, out InputFieldScroller consoleScroll, fontSize);
var inputField = consoleScroll.inputField;
var mainTextObj = inputField.textComponent.gameObject;
var mainTextInput = inputField.textComponent;
mainTextInput.supportRichText = false;
mainTextInput.color = new Color(1, 1, 1, 0.5f);
var placeHolderText = inputField.placeholder.GetComponent<Text>();
placeHolderText.text = STARTUP_TEXT;
placeHolderText.fontSize = fontSize;
var highlightTextObj = UIFactory.CreateUIObject("HighlightText", mainTextObj.gameObject);
var highlightTextRect = highlightTextObj.GetComponent<RectTransform>();
highlightTextRect.pivot = new Vector2(0, 1);
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = new Vector2(20, 0);
highlightTextRect.offsetMax = new Vector2(14, 0);
var highlightTextInput = highlightTextObj.AddComponent<Text>();
highlightTextInput.supportRichText = true;
highlightTextInput.fontSize = fontSize;
#endregion
#region COMPILE BUTTON
var compileBtnObj = UIFactory.CreateButton(ConsolePage.Instance.Content);
var compileBtnLayout = compileBtnObj.AddComponent<LayoutElement>();
compileBtnLayout.preferredWidth = 80;
compileBtnLayout.flexibleWidth = 0;
compileBtnLayout.minHeight = 45;
compileBtnLayout.flexibleHeight = 0;
var compileButton = compileBtnObj.GetComponent<Button>();
var compileBtnColors = compileButton.colors;
compileBtnColors.normalColor = new Color(14f / 255f, 80f / 255f, 14f / 255f);
compileButton.colors = compileBtnColors;
var btnText = compileBtnObj.GetComponentInChildren<Text>();
btnText.text = "Run";
btnText.fontSize = 18;
btnText.color = Color.white;
// Set compile button callback now that we have the Input Field reference
compileButton.onClick.AddListener(CompileCallback);
void CompileCallback()
{
if (!string.IsNullOrEmpty(inputField.text))
{
ConsolePage.Instance.Evaluate(inputField.text.Trim());
}
}
#endregion
//mainTextInput.supportRichText = false;
mainTextInput.font = UIManager.ConsoleFont;
placeHolderText.font = UIManager.ConsoleFont;
highlightTextInput.font = UIManager.ConsoleFont;
// reset this after formatting finalized
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = Vector2.zero;
highlightTextRect.offsetMax = Vector2.zero;
// assign references
this.InputField = inputField;
this.InputText = mainTextInput;
this.inputHighlightText = highlightTextInput;
}
}
}

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexer
{
public class CommentMatch : Matcher
{
public string lineCommentStart = @"//";
public string blockCommentStart = @"/*";
public string blockCommentEnd = @"*/";
public override Color HighlightColor => new Color(0.34f, 0.65f, 0.29f, 1.0f);
public override IEnumerable<char> StartChars => new char[] { lineCommentStart[0], blockCommentStart[0] };
public override IEnumerable<char> EndChars => new char[] { blockCommentEnd[0] };
public override bool IsImplicitMatch(CSharpLexer lexer) => IsMatch(lexer, lineCommentStart) || IsMatch(lexer, blockCommentStart);
private bool IsMatch(CSharpLexer lexer, string commentType)
{
if (!string.IsNullOrEmpty(commentType))
{
lexer.Rollback();
bool match = true;
for (int i = 0; i < commentType.Length; i++)
{
if (commentType[i] != lexer.ReadNext())
{
match = false;
break;
}
}
if (match)
{
// Read until end of line or file
while (!IsEndLineOrEndFile(lexer, lexer.ReadNext())) { }
return true;
}
}
return false;
}
private bool IsEndLineOrEndFile(CSharpLexer lexer, char character) => lexer.EndOfStream || character == '\n' || character == '\r';
}
}

View File

@ -0,0 +1,97 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexer
{
// I use two different KeywordMatch instances (valid and invalid).
// This class just contains common implementations.
public class KeywordMatch : Matcher
{
public string[] Keywords;
public override Color HighlightColor => highlightColor;
public Color highlightColor;
private readonly HashSet<string> shortlist = new HashSet<string>();
private readonly Stack<string> removeList = new Stack<string>();
public override bool IsImplicitMatch(CSharpLexer lexer)
{
if (!char.IsWhiteSpace(lexer.Previous) &&
!lexer.IsSpecialSymbol(lexer.Previous, DelimiterType.End))
{
return false;
}
shortlist.Clear();
int currentIndex = 0;
char currentChar = lexer.ReadNext();
for (int i = 0; i < Keywords.Length; i++)
{
if (Keywords[i][0] == currentChar)
{
shortlist.Add(Keywords[i]);
}
}
if (shortlist.Count == 0)
{
return false;
}
do
{
if (lexer.EndOfStream)
{
RemoveLongStrings(currentIndex + 1);
break;
}
currentChar = lexer.ReadNext();
currentIndex++;
if (char.IsWhiteSpace(currentChar) ||
lexer.IsSpecialSymbol(currentChar, DelimiterType.Start))
{
RemoveLongStrings(currentIndex);
lexer.Rollback(1);
break;
}
foreach (string keyword in shortlist)
{
if (currentIndex >= keyword.Length || keyword[currentIndex] != currentChar)
{
removeList.Push(keyword);
}
}
while (removeList.Count > 0)
{
shortlist.Remove(removeList.Pop());
}
}
while (shortlist.Count > 0);
return shortlist.Count > 0;
}
private void RemoveLongStrings(int length)
{
foreach (string keyword in shortlist)
{
if (keyword.Length > length)
{
removeList.Push(keyword);
}
}
while (removeList.Count > 0)
{
shortlist.Remove(removeList.Pop());
}
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using UnityExplorer.Unstrip;
using UnityEngine;
using System.Linq;
namespace UnityExplorer.CSConsole.Lexer
{
public abstract class Matcher
{
public abstract Color HighlightColor { get; }
public string HexColor => htmlColor ?? (htmlColor = "<color=#" + HighlightColor.ToHex() + ">");
private string htmlColor;
public virtual IEnumerable<char> StartChars => Enumerable.Empty<char>();
public virtual IEnumerable<char> EndChars => Enumerable.Empty<char>();
public abstract bool IsImplicitMatch(CSharpLexer lexer);
public bool IsMatch(CSharpLexer lexer)
{
if (IsImplicitMatch(lexer))
{
lexer.Commit();
return true;
}
lexer.Rollback();
return false;
}
}
}

View File

@ -0,0 +1,39 @@
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexer
{
public class NumberMatch : Matcher
{
public override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f);
public override bool IsImplicitMatch(CSharpLexer lexer)
{
if (!char.IsWhiteSpace(lexer.Previous) &&
!lexer.IsSpecialSymbol(lexer.Previous, DelimiterType.End))
{
return false;
}
bool matchedNumber = false;
while (!lexer.EndOfStream)
{
if (IsNumberOrDecimalPoint(lexer.ReadNext()))
{
matchedNumber = true;
lexer.Commit();
}
else
{
lexer.Rollback();
break;
}
}
return matchedNumber;
}
private bool IsNumberOrDecimalPoint(char character) => char.IsNumber(character) || character == '.';
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexer
{
public class StringMatch : Matcher
{
public override Color HighlightColor => new Color(0.79f, 0.52f, 0.32f, 1.0f);
public override IEnumerable<char> StartChars => new[] { '"' };
public override IEnumerable<char> EndChars => new[] { '"' };
public override bool IsImplicitMatch(CSharpLexer lexer)
{
if (lexer.ReadNext() == '"')
{
while (!IsClosingQuoteOrEndFile(lexer, lexer.ReadNext())) { }
return true;
}
return false;
}
private bool IsClosingQuoteOrEndFile(CSharpLexer lexer, char character) => lexer.EndOfStream || character == '"';
}
}

View File

@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityExplorer.CSConsole.Lexer
{
public class SymbolMatch : Matcher
{
public override Color HighlightColor => new Color(0.58f, 0.47f, 0.37f, 1.0f);
private readonly string[] symbols = new[]
{
"[", "]", "(", ")", ".", "?", ":", "+", "-", "*", "/", "%", "&", "|", "^", "~", "=", "<", ">",
"++", "--", "&&", "||", "<<", ">>", "==", "!=", "<=", ">=", "+=", "-=", "*=", "/=", "%=", "&=",
"|=", "^=", "<<=", ">>=", "->", "??", "=>",
};
private static readonly List<string> shortlist = new List<string>();
private static readonly Stack<string> removeList = new Stack<string>();
public override IEnumerable<char> StartChars => symbols.Select(s => s[0]);
public override IEnumerable<char> EndChars => symbols.Select(s => s[0]);
public override bool IsImplicitMatch(CSharpLexer lexer)
{
if (lexer == null)
return false;
if (!char.IsWhiteSpace(lexer.Previous) &&
!char.IsLetter(lexer.Previous) &&
!char.IsDigit(lexer.Previous) &&
!lexer.IsSpecialSymbol(lexer.Previous, DelimiterType.End))
{
return false;
}
shortlist.Clear();
int currentIndex = 0;
char currentChar = lexer.ReadNext();
for (int i = symbols.Length - 1; i >= 0; i--)
{
if (symbols[i][0] == currentChar)
shortlist.Add(symbols[i]);
}
if (shortlist.Count == 0)
return false;
do
{
if (lexer.EndOfStream)
{
RemoveLongStrings(currentIndex + 1);
break;
}
currentChar = lexer.ReadNext();
currentIndex++;
if (char.IsWhiteSpace(currentChar) ||
char.IsLetter(currentChar) ||
char.IsDigit(currentChar) ||
lexer.IsSpecialSymbol(currentChar, DelimiterType.Start))
{
RemoveLongStrings(currentIndex);
lexer.Rollback(1);
break;
}
foreach (string symbol in shortlist)
{
if (currentIndex >= symbol.Length || symbol[currentIndex] != currentChar)
{
removeList.Push(symbol);
}
}
while (removeList.Count > 0)
{
shortlist.Remove(removeList.Pop());
}
}
while (shortlist.Count > 0);
return shortlist.Count > 0;
}
private void RemoveLongStrings(int length)
{
foreach (string keyword in shortlist)
{
if (keyword.Length > length)
{
removeList.Push(keyword);
}
}
while (removeList.Count > 0)
{
shortlist.Remove(removeList.Pop());
}
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Mono.CSharp;
// 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"
};
private readonly TextWriter tw;
public ScriptEvaluator(TextWriter tw) : base(BuildContext(tw))
{
this.tw = tw;
ImportAppdomainAssemblies(ReferenceAssembly);
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
}
public void Dispose()
{
AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad;
tw.Dispose();
}
private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
string name = args.LoadedAssembly.GetName().Name;
if (StdLib.Contains(name))
{
return;
}
ReferenceAssembly(args.LoadedAssembly);
}
private static CompilerContext BuildContext(TextWriter tw)
{
var reporter = 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, reporter);
}
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,57 @@
using System;
using Mono.CSharp;
using UnityExplorer.UI;
using UnityExplorer.UI.Modules;
using UnityExplorer.Inspectors;
namespace UnityExplorer.CSConsole
{
public class ScriptInteraction : InteractiveBase
{
public static void Log(object message)
{
ExplorerCore.Log(message);
}
public static void AddUsing(string directive)
{
ConsolePage.Instance.AddUsing(directive);
}
public static void GetUsing()
{
ExplorerCore.Log(ConsolePage.Instance.m_evaluator.GetUsing());
}
public static void Reset()
{
ConsolePage.Instance.ResetConsole();
}
public static object CurrentTarget()
{
return InspectorManager.Instance?.m_activeInspector?.Target;
}
public static object[] AllTargets()
{
int count = InspectorManager.Instance?.m_currentInspectors.Count ?? 0;
object[] ret = new object[count];
for (int i = 0; i < count; i++)
{
ret[i] = InspectorManager.Instance?.m_currentInspectors[i].Target;
}
return ret;
}
public static void Inspect(object obj)
{
InspectorManager.Instance.Inspect(obj);
}
public static void Inspect(Type type)
{
InspectorManager.Instance.Inspect(type);
}
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityExplorer.Helpers;
namespace UnityExplorer.CSConsole
{
public struct Suggestion
{
public enum Contexts
{
Namespace,
Keyword,
Other
}
// ~~~~ Instance ~~~~
public readonly string Prefix;
public readonly string Addition;
public readonly Contexts Context;
public string Full => Prefix + Addition;
public Color TextColor => GetTextColor();
public Suggestion(string addition, string prefix, Contexts type)
{
Addition = addition;
Prefix = prefix;
Context = type;
}
private Color GetTextColor()
{
switch (Context)
{
case Contexts.Namespace: return Color.grey;
case Contexts.Keyword: return keywordColor;
default: return Color.white;
}
}
// ~~~~ Static ~~~~
public static HashSet<string> Namespaces => m_namspaces ?? GetNamespaces();
private static HashSet<string> m_namspaces;
public static HashSet<string> Keywords => m_keywords ?? (m_keywords = new HashSet<string>(CSharpLexer.validKeywordMatcher.Keywords));
private static HashSet<string> m_keywords;
private static readonly Color keywordColor = new Color(80f / 255f, 150f / 255f, 215f / 255f);
private static HashSet<string> GetNamespaces()
{
HashSet<string> set = new HashSet<string>(
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(GetTypes)
.Where(x => x.IsPublic && !string.IsNullOrEmpty(x.Namespace))
.Select(x => x.Namespace));
return m_namspaces = set;
IEnumerable<Type> GetTypes(Assembly asm) => asm.TryGetTypes();
}
}
}