This commit is contained in:
sinaioutlander
2020-11-05 17:33:04 +11:00
parent a46bc11e42
commit e175e9c438
47 changed files with 890 additions and 336 deletions

View File

@ -0,0 +1,320 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.UI;
using UnityExplorer.UI.PageModel;
namespace UnityExplorer.Console
{
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 (!ConsolePage.EnableSuggestions)
{
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()
{
var editor = ConsolePage.Instance.m_codeEditor;
if (editor.InputField.text.Length < 1)
{
return;
}
int caretPos = editor.InputField.caretPosition;
while (caretPos >= editor.inputText.textInfo.characterInfo.Length)
{
caretPos--;
}
if (caretPos == m_lastCaretPos)
{
return;
}
m_lastCaretPos = caretPos;
if (caretPos >= 0 && caretPos < editor.inputText.textInfo.characterInfo.Length)
{
var pos = editor.inputText.textInfo.characterInfo[caretPos].bottomLeft;
pos = editor.InputField.transform.TransformPoint(pos);
m_mainObj.transform.position = new Vector3(pos.x, pos.y - 3, 0);
}
}
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 (ConsolePage.AutoCompletes.Any())
{
ConsolePage.AutoCompletes.Clear();
}
}
public static void GetAutocompletes(string input)
{
try
{
// Credit ManylMarco
ConsolePage.AutoCompletes.Clear();
string[] completions = ConsolePage.Instance.m_evaluator.GetCompletions(input, out string prefix);
if (completions != null)
{
if (prefix == null)
{
prefix = input;
}
ConsolePage.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));
ConsolePage.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));
ConsolePage.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, 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);
#if CPP
btn.onClick.AddListener(new Action(UseAutocompleteButton));
#else
btn.onClick.AddListener(UseAutocompleteButton);
#endif
void UseAutocompleteButton()
{
ConsolePage.Instance.UseAutocomplete(hiddenText.text);
EventSystem.current.SetSelectedGameObject(ConsolePage.Instance.m_codeEditor.InputField.gameObject,
null);
}
m_suggestionButtons.Add(buttonObj);
m_suggestionTexts.Add(text);
}
}
#endregion
}
}

181
src/Console/CSharpLexer.cs Normal file
View File

@ -0,0 +1,181 @@
using System.Collections.Generic;
using System.Text;
using UnityExplorer.Console.Lexer;
using UnityEngine;
namespace UnityExplorer.Console
{
public static class CSharpLexer
{
public static char indentIncreaseCharacter = '{';
public static char indentDecreaseCharacter = '}';
public static string delimiterSymbols = "[ ] ( ) { } ; : , .";
private static readonly StringBuilder indentBuilder = new StringBuilder();
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 = @"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 = @"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"
};
private static char[] delimiterSymbolCache = null;
internal static char[] DelimiterSymbols
{
get
{
if (delimiterSymbolCache == null)
{
string[] symbols = delimiterSymbols.Split(' ');
int count = 0;
for (int i = 0; i < symbols.Length; i++)
{
if (symbols[i].Length == 1)
{
count++;
}
}
delimiterSymbolCache = new char[count];
for (int i = 0, index = 0; i < symbols.Length; i++)
{
if (symbols[i].Length == 1)
{
delimiterSymbolCache[index] = symbols[i][0];
index++;
}
}
}
return delimiterSymbolCache;
}
}
private static Matcher[] matchers = null;
internal static Matcher[] Matchers
{
get
{
if (matchers == null)
{
List<Matcher> matcherList = new List<Matcher>
{
commentMatcher,
symbolMatcher,
numberMatcher,
stringMatcher,
validKeywordMatcher,
invalidKeywordMatcher,
};
matchers = matcherList.ToArray();
}
return matchers;
}
}
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] == indentIncreaseCharacter)
{
indentBuilder.Append(indentIncreaseCharacter);
indent++;
}
else if (!stringState && input[i] == indentDecreaseCharacter)
{
indentBuilder.Append(indentDecreaseCharacter);
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;
}
}
}

443
src/Console/CodeEditor.cs Normal file
View File

@ -0,0 +1,443 @@
using System;
using System.Linq;
using System.Text;
using UnityExplorer.Input;
using UnityExplorer.Console.Lexer;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityExplorer.UI;
using UnityExplorer.UI.PageModel;
namespace UnityExplorer.Console
{
public class CodeEditor
{
private readonly InputLexer inputLexer = new InputLexer();
public TMP_InputField InputField { get; }
public readonly TextMeshProUGUI inputText;
private readonly TextMeshProUGUI inputHighlightText;
private readonly TextMeshProUGUI lineText;
private readonly Image background;
private readonly Image lineHighlight;
private readonly Image lineNumberBackground;
private readonly Image scrollbar;
private bool lineHighlightLocked;
private readonly RectTransform inputTextTransform;
private readonly RectTransform lineHighlightTransform;
public int LineCount { get; private set; }
public int CurrentLine { get; private set; }
public int CurrentColumn { get; private set; }
public int CurrentIndent { get; private set; }
private static readonly StringBuilder highlightedBuilder = new StringBuilder(4096);
private static readonly StringBuilder lineBuilder = new StringBuilder();
private static readonly KeyCode[] lineChangeKeys =
{
KeyCode.Return, KeyCode.Backspace, KeyCode.UpArrow,
KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow
};
public string HighlightedText => inputHighlightText.text;
public string Text
{
get { return InputField.text; }
set
{
if (!string.IsNullOrEmpty(value))
{
InputField.text = value;
inputHighlightText.text = value;
}
else
{
InputField.text = string.Empty;
inputHighlightText.text = string.Empty;
}
inputText.ForceMeshUpdate(false);
}
}
public CodeEditor(TMP_InputField inputField, TextMeshProUGUI inputText, TextMeshProUGUI inputHighlightText, TextMeshProUGUI lineText,
Image background, Image lineHighlight, Image lineNumberBackground, Image scrollbar)
{
InputField = inputField;
this.inputText = inputText;
this.inputHighlightText = inputHighlightText;
this.lineText = lineText;
this.background = background;
this.lineHighlight = lineHighlight;
this.lineNumberBackground = lineNumberBackground;
this.scrollbar = scrollbar;
if (!AllReferencesAssigned())
{
throw new Exception("References are missing!");
}
InputField.restoreOriginalTextOnEscape = false;
inputTextTransform = inputText.GetComponent<RectTransform>();
lineHighlightTransform = lineHighlight.GetComponent<RectTransform>();
ApplyTheme();
inputLexer.UseMatchers(CSharpLexer.DelimiterSymbols, CSharpLexer.Matchers);
// subscribe to text input changing
#if CPP
InputField.onValueChanged.AddListener(new Action<string>((string s) => { OnInputChanged(); }));
#else
this.InputField.onValueChanged.AddListener((string s) => { OnInputChanged(); });
#endif
}
public void Update()
{
// Check for new line
if (ConsolePage.EnableAutoIndent && InputManager.GetKeyDown(KeyCode.Return))
{
AutoIndentCaret();
}
if (EventSystem.current?.currentSelectedGameObject?.name == "InputField (TMP)")
{
bool focusKeyPressed = false;
// Check for any focus key pressed
foreach (KeyCode key in lineChangeKeys)
{
if (InputManager.GetKeyDown(key))
{
focusKeyPressed = true;
break;
}
}
// Update line highlight
if (focusKeyPressed || InputManager.GetMouseButton(0))
{
UpdateHighlight();
ConsolePage.Instance.OnInputChanged();
}
}
}
public void OnInputChanged(bool forceUpdate = false)
{
string newText = InputField.text;
UpdateIndent();
if (!forceUpdate && string.IsNullOrEmpty(newText))
{
inputHighlightText.text = string.Empty;
}
else
{
inputHighlightText.text = SyntaxHighlightContent(newText);
}
UpdateLineNumbers();
UpdateHighlight();
ConsolePage.Instance.OnInputChanged();
}
public void SetLineHighlight(int lineNumber, bool lockLineHighlight)
{
if (lineNumber < 1 || lineNumber > LineCount)
{
return;
}
lineHighlightTransform.anchoredPosition = new Vector2(5,
(inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight *
-(lineNumber - 1)) - 4f +
inputTextTransform.anchoredPosition.y);
lineHighlightLocked = lockLineHighlight;
}
private void UpdateLineNumbers()
{
int currentLineCount = inputText.textInfo.lineCount;
int currentLineNumber = 1;
if (currentLineCount != LineCount)
{
try
{
lineBuilder.Length = 0;
for (int i = 1; i < currentLineCount + 2; i++)
{
if (i - 1 > 0 && i - 1 < currentLineCount - 1)
{
int characterStart = inputText.textInfo.lineInfo[i - 1].firstCharacterIndex;
int characterCount = inputText.textInfo.lineInfo[i - 1].characterCount;
if (characterStart >= 0 && characterStart < inputText.text.Length &&
characterCount != 0 && !inputText.text.Substring(characterStart, characterCount).Contains("\n"))
{
lineBuilder.Append("\n");
continue;
}
}
lineBuilder.Append(currentLineNumber);
lineBuilder.Append('\n');
currentLineNumber++;
if (i - 1 == 0 && i - 1 < currentLineCount - 1)
{
int characterStart = inputText.textInfo.lineInfo[i - 1].firstCharacterIndex;
int characterCount = inputText.textInfo.lineInfo[i - 1].characterCount;
if (characterStart >= 0 && characterStart < inputText.text.Length &&
characterCount != 0 && !inputText.text.Substring(characterStart, characterCount).Contains("\n"))
{
lineBuilder.Append("\n");
continue;
}
}
}
lineText.text = lineBuilder.ToString();
LineCount = currentLineCount;
}
catch { }
}
}
private void UpdateIndent()
{
int caret = InputField.caretPosition;
if (caret < 0 || caret >= inputText.textInfo.characterInfo.Length)
{
while (caret >= 0 && caret >= inputText.textInfo.characterInfo.Length)
{
caret--;
}
if (caret < 0 || caret >= inputText.textInfo.characterInfo.Length)
{
return;
}
}
CurrentLine = inputText.textInfo.characterInfo[caret].lineNumber;
int charCount = 0;
for (int i = 0; i < CurrentLine; i++)
{
charCount += inputText.textInfo.lineInfo[i].characterCount;
}
CurrentColumn = caret - charCount;
CurrentIndent = 0;
for (int i = 0; i < caret && i < InputField.text.Length; i++)
{
char character = InputField.text[i];
if (character == CSharpLexer.indentIncreaseCharacter)
{
CurrentIndent++;
}
if (character == CSharpLexer.indentDecreaseCharacter)
{
CurrentIndent--;
}
}
if (CurrentIndent < 0)
{
CurrentIndent = 0;
}
}
private void UpdateHighlight()
{
if (lineHighlightLocked)
{
return;
}
try
{
int caret = InputField.caretPosition - 1;
float lineHeight = inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight;
int lineNumber = inputText.textInfo.characterInfo[caret].lineNumber;
float offset = lineNumber + inputTextTransform.anchoredPosition.y;
lineHighlightTransform.anchoredPosition = new Vector2(5, -(offset * lineHeight));
}
catch //(Exception e)
{
//ExplorerCore.LogWarning("Exception on Update Line Highlight: " + e);
}
}
private const string CLOSE_COLOR_TAG = "</color>";
private string SyntaxHighlightContent(string inputText)
{
int offset = 0;
highlightedBuilder.Length = 0;
foreach (LexerMatchInfo match in inputLexer.LexInputString(inputText))
{
for (int i = offset; i < match.startIndex; i++)
{
highlightedBuilder.Append(inputText[i]);
}
highlightedBuilder.Append(match.htmlColor);
for (int i = match.startIndex; i < match.endIndex; i++)
{
highlightedBuilder.Append(inputText[i]);
}
highlightedBuilder.Append(CLOSE_COLOR_TAG);
offset = match.endIndex;
}
for (int i = offset; i < inputText.Length; i++)
{
highlightedBuilder.Append(inputText[i]);
}
inputText = highlightedBuilder.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.indentIncreaseCharacter).Count();
int numClose = InputField.text.Where(x => x == CSharpLexer.indentDecreaseCharacter).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;
}
}
// Update line column and indent positions
UpdateIndent();
inputText.text = InputField.text;
inputText.SetText(InputField.text, true);
inputText.Rebuild(CanvasUpdate.Prelayout);
InputField.ForceLabelUpdate();
InputField.Rebuild(CanvasUpdate.Prelayout);
OnInputChanged(true);
}
private string GetAutoIndentTab(int amount)
{
string tab = string.Empty;
for (int i = 0; i < amount; i++)
{
tab += "\t";
}
return tab;
}
private static Color caretColor = new Color32(255, 255, 255, 255);
private static Color textColor = new Color32(255, 255, 255, 255);
private static Color backgroundColor = new Color32(37, 37, 37, 255);
private static Color lineHighlightColor = new Color32(50, 50, 50, 255);
private static Color lineNumberBackgroundColor = new Color32(25, 25, 25, 255);
private static Color lineNumberTextColor = new Color32(180, 180, 180, 255);
private static Color scrollbarColor = new Color32(45, 50, 50, 255);
private void ApplyTheme()
{
var highlightTextRect = inputHighlightText.GetComponent<RectTransform>();
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = Vector2.zero;
highlightTextRect.offsetMax = Vector2.zero;
InputField.caretColor = caretColor;
inputText.color = textColor;
inputHighlightText.color = textColor;
background.color = backgroundColor;
lineHighlight.color = lineHighlightColor;
lineNumberBackground.color = lineNumberBackgroundColor;
lineText.color = lineNumberTextColor;
scrollbar.color = scrollbarColor;
}
private bool AllReferencesAssigned()
{
if (!InputField ||
!inputText ||
!inputHighlightText ||
!lineText ||
!background ||
!lineHighlight ||
!lineNumberBackground ||
!scrollbar)
{
// One or more references are not assigned
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.Console.Lexer
{
public sealed 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(InputLexer lexer) => IsMatch(lexer, lineCommentStart) || IsMatch(lexer, blockCommentStart);
private bool IsMatch(InputLexer 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
while (!IsEndLineOrEndFile(lexer, lexer.ReadNext()))
{
;
}
return true;
}
}
return false;
}
private bool IsEndLineOrEndFile(InputLexer lexer, char character) => lexer.EndOfStream || character == '\n' || character == '\r';
}
}

View File

@ -0,0 +1,201 @@
using System.Collections.Generic;
namespace UnityExplorer.Console.Lexer
{
public struct LexerMatchInfo
{
public int startIndex;
public int endIndex;
public string htmlColor;
}
public enum SpecialCharacterPosition
{
Start,
End,
};
public class InputLexer
{
private string inputString = null;
private Matcher[] matchers = null;
private readonly HashSet<char> specialStartSymbols = new HashSet<char>();
private readonly HashSet<char> specialEndSymbols = new HashSet<char>();
private int currentIndex = 0;
private int currentLookaheadIndex = 0;
private char current = ' ';
public char Previous { get; private set; } = ' ';
public bool EndOfStream
{
get { return currentLookaheadIndex >= inputString.Length; }
}
public void UseMatchers(char[] delimiters, Matcher[] matchers)
{
this.matchers = matchers;
specialStartSymbols.Clear();
specialEndSymbols.Clear();
if (delimiters != null)
{
foreach (char character in delimiters)
{
if (!specialStartSymbols.Contains(character))
{
specialStartSymbols.Add(character);
}
if (!specialEndSymbols.Contains(character))
{
specialEndSymbols.Add(character);
}
}
}
if (matchers != null)
{
foreach (Matcher lexer in matchers)
{
foreach (char special in lexer.StartChars)
{
if (!specialStartSymbols.Contains(special))
{
specialStartSymbols.Add(special);
}
}
foreach (char special in lexer.EndChars)
{
if (!specialEndSymbols.Contains(special))
{
specialEndSymbols.Add(special);
}
}
}
}
}
public IEnumerable<LexerMatchInfo> LexInputString(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();
}
}
}
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, SpecialCharacterPosition position = SpecialCharacterPosition.Start)
{
if (position == SpecialCharacterPosition.Start)
{
return specialStartSymbols.Contains(character);
}
return specialEndSymbols.Contains(character);
}
private void ReadWhiteSpace()
{
while (char.IsWhiteSpace(ReadNext()) == true)
{
Commit();
}
Rollback();
}
}
}

View File

@ -0,0 +1,116 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.Console.Lexer
{
public sealed 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 string[] keywordCache = null;
public override bool IsImplicitMatch(InputLexer lexer)
{
BuildKeywordCache();
if (!char.IsWhiteSpace(lexer.Previous) &&
!lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End))
{
return false;
}
shortlist.Clear();
int currentIndex = 0;
char currentChar = lexer.ReadNext();
for (int i = 0; i < keywordCache.Length; i++)
{
if (keywordCache[i][0] == currentChar)
{
shortlist.Add(keywordCache[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, SpecialCharacterPosition.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());
}
}
private void BuildKeywordCache()
{
if (keywordCache == null)
{
string[] kwSplit = keywords.Split(' ');
List<string> list = new List<string>();
foreach (string kw in kwSplit)
{
if (!string.IsNullOrEmpty(kw) && kw.Length > 0)
{
list.Add(kw);
}
}
keywordCache = list.ToArray();
}
}
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using UnityExplorer.Unstrip.ColorUtility;
using UnityEngine;
namespace UnityExplorer.Console.Lexer
{
public abstract class Matcher
{
public abstract Color HighlightColor { get; }
public string HexColor => htmlColor ?? (htmlColor = "<#" + HighlightColor.ToHex() + ">");
private string htmlColor = null;
public virtual IEnumerable<char> StartChars { get { yield break; } }
public virtual IEnumerable<char> EndChars { get { yield break; } }
public abstract bool IsImplicitMatch(InputLexer lexer);
public bool IsMatch(InputLexer lexer)
{
if (IsImplicitMatch(lexer))
{
lexer.Commit();
return true;
}
lexer.Rollback();
return false;
}
}
}

View File

@ -0,0 +1,39 @@
using UnityEngine;
namespace UnityExplorer.Console.Lexer
{
public sealed class NumberMatch : Matcher
{
public override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f);
public override bool IsImplicitMatch(InputLexer lexer)
{
if (!char.IsWhiteSpace(lexer.Previous) &&
!lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.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,37 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityExplorer.Console.Lexer
{
public sealed class StringMatch : Matcher
{
public override Color HighlightColor => new Color(0.79f, 0.52f, 0.32f, 1.0f);
public override IEnumerable<char> StartChars { get { yield return '"'; } }
public override IEnumerable<char> EndChars { get { yield return '"'; } }
public override bool IsImplicitMatch(InputLexer lexer)
{
if (lexer.ReadNext() == '"')
{
while (!IsClosingQuoteOrEndFile(lexer, lexer.ReadNext()))
{
;
}
return true;
}
return false;
}
private bool IsClosingQuoteOrEndFile(InputLexer lexer, char character)
{
if (lexer.EndOfStream == true ||
character == '"')
{
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,151 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityExplorer.Console.Lexer
{
public sealed class SymbolMatch : Matcher
{
public override Color HighlightColor => new Color(0.58f, 0.47f, 0.37f, 1.0f);
public string Symbols => @"[ ] ( ) . ? : + - * / % & | ^ ~ = < > ++ -- && || << >> == != <= >=
+= -= *= /= %= &= |= ^= <<= >>= -> ?? =>";
private static readonly List<string> shortlist = new List<string>();
private static readonly Stack<string> removeList = new Stack<string>();
private string[] symbolCache = null;
public override IEnumerable<char> StartChars
{
get
{
BuildSymbolCache();
foreach (string symbol in symbolCache.Where(x => x.Length > 0))
{
yield return symbol[0];
}
}
}
public override IEnumerable<char> EndChars
{
get
{
BuildSymbolCache();
foreach (string symbol in symbolCache.Where(x => x.Length > 0))
{
yield return symbol[0];
}
}
}
public override bool IsImplicitMatch(InputLexer lexer)
{
if (lexer == null)
{
return false;
}
BuildSymbolCache();
if (!char.IsWhiteSpace(lexer.Previous) &&
!char.IsLetter(lexer.Previous) &&
!char.IsDigit(lexer.Previous) &&
!lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End))
{
return false;
}
shortlist.Clear();
int currentIndex = 0;
char currentChar = lexer.ReadNext();
for (int i = symbolCache.Length - 1; i >= 0; i--)
{
if (symbolCache[i][0] == currentChar)
{
shortlist.Add(symbolCache[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, SpecialCharacterPosition.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());
}
}
private void BuildSymbolCache()
{
if (symbolCache != null)
{
return;
}
string[] symSplit = Symbols.Split(' ');
List<string> list = new List<string>();
foreach (string sym in symSplit)
{
if (!string.IsNullOrEmpty(sym) && sym.Length > 0)
{
list.Add(sym);
}
}
symbolCache = list.ToArray();
}
}
}

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.Console
{
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 _logger;
public ScriptEvaluator(TextWriter logger) : base(BuildContext(logger))
{
_logger = logger;
ImportAppdomainAssemblies(ReferenceAssembly);
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
}
public void Dispose()
{
AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad;
_logger.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.PageModel;
using UnityExplorer.Inspectors;
namespace UnityExplorer.Console
{
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);
}
}
}

82
src/Console/Suggestion.cs Normal file
View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityExplorer.Helpers;
namespace UnityExplorer.Console
{
public struct Suggestion
{
public string Full => Prefix + Addition;
public readonly string Prefix;
public readonly string Addition;
public readonly Contexts Context;
public Color TextColor
{
get
{
switch (Context)
{
case Contexts.Namespace: return Color.grey;
case Contexts.Keyword: return systemBlue;
default: return Color.white;
}
}
}
private static readonly Color systemBlue = new Color(80f / 255f, 150f / 255f, 215f / 255f);
public Suggestion(string addition, string prefix, Contexts type)
{
Addition = addition;
Prefix = prefix;
Context = type;
}
public enum Contexts
{
Namespace,
Keyword,
Other
}
public static HashSet<string> Namespaces => m_namspaces ?? GetNamespaces();
private static HashSet<string> m_namspaces;
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();
}
public static HashSet<string> Keywords => m_keywords ?? GetKeywords();
private static HashSet<string> m_keywords;
private static HashSet<string> GetKeywords()
{
if (CSharpLexer.validKeywordMatcher.keywordCache == null)
{
return new HashSet<string>();
}
HashSet<string> set = new HashSet<string>();
foreach (string keyword in CSharpLexer.validKeywordMatcher.keywordCache)
{
set.Add(keyword);
}
return m_keywords = set;
}
}
}