developed new C# console

Derived heavily from notepad++ and Unity In-Game Code Editor, work will be done to refactor and strip down the code, most of it is unnecessary for our needs anyway. Temporary credit to IGCE for most of it.
This commit is contained in:
sinaioutlander 2020-10-25 20:57:34 +11:00
parent 0d4b4dc826
commit 648ac941df
22 changed files with 2657 additions and 22 deletions

View File

@ -256,6 +256,21 @@
<Compile Include="UI\Main\MainMenu.cs" />
<Compile Include="UI\Main\Pages\BaseMenuPage.cs" />
<Compile Include="UI\Main\Pages\ConsolePage.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\AutoIndent.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\CodeEditor.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\CodeTheme.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\InputTheme.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\CommentGroupMatch.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\ILexer.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\InputStringLexer.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\KeywordGroupMatch.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\LiteralGroupMatch.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\MatchLexer.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\NumberGroupMatch.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\SymbolGroupMatch.cs" />
<Compile Include="UI\Main\Pages\Console\ScriptEvaluator\AutoComplete.cs" />
<Compile Include="UI\Main\Pages\Console\ScriptEvaluator\ScriptEvaluator.cs" />
<Compile Include="UI\Main\Pages\Console\ScriptEvaluator\ScriptInteraction.cs" />
<Compile Include="UI\Main\Pages\HomePage.cs" />
<Compile Include="UI\Main\Pages\OptionsPage.cs" />
<Compile Include="UI\Main\Pages\SearchPage.cs" />

View File

@ -97,10 +97,6 @@ namespace ExplorerBeta
m_doneUIInit = true;
}
}
else
{
UIManager.Update();
}
if (InputManager.GetKeyDown(ModConfig.Instance.Main_Menu_Toggle))
{

View File

@ -94,14 +94,6 @@ namespace ExplorerBeta.UI.Main
tmpInput.verticalScrollbar = scroller;
m_textInput = input.GetComponent<TMP_InputField>();
for (int i = 0; i < 100; i++)
{
Log("hello " + i);
}
Log("hello", Color.red);
Log("hello", Color.yellow);
}
public static void Log(string message)

View File

@ -49,6 +49,7 @@ namespace ExplorerBeta.UI.Main
foreach (var page in Pages)
{
page.Init();
page.Content?.SetActive(false);
}
SetPage(Pages[0]);
@ -56,7 +57,7 @@ namespace ExplorerBeta.UI.Main
public void Update()
{
// todo
m_activePage?.Update();
}
// todo

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Explorer.UI.Main.Pages.Console
{
public class AutoIndent
{
// Enum
public enum IndentMode
{
None,
AutoTab,
AutoTabContextual,
}
// Private
private static readonly StringBuilder indentBuilder = new StringBuilder();
private static string indentDecreaseString = null;
// Public
public static IndentMode autoIndentMode = IndentMode.AutoTabContextual;
/// <summary>
/// Should auto indent be used for this language.
/// </summary>
public static bool allowAutoIndent = true;
/// <summary>
/// The character that causes the indent level to increase.
/// </summary>
public static char indentIncreaseCharacter = '{';
/// <summary>
/// The character that causes the indent level to decrease.
/// </summary>
public static char indentDecreaseCharacter = '}';
// Properties
/// <summary>
/// Get the string representation of the indent character.
/// </summary>
public static string IndentDecreaseString
{
get
{
if (indentDecreaseString == null)
{
indentDecreaseString = new string(indentDecreaseCharacter, 1);
}
return indentDecreaseString;
}
}
// Methods
public static string GetAutoIndentedFormattedString(string indentSection, int currentIndent, out int caretPosition)
{
// Add indent level
int indent = currentIndent + 1;
// Append characters
for (int i = 0; i < indentSection.Length; i++)
{
if (indentSection[i] == '\n')
{
indentBuilder.Append('\n');
AppendIndentString(indent);
}
else if (indentSection[i] == '\t')
{
// We will add tabs manually
continue;
}
else if (indentSection[i] == indentIncreaseCharacter)
{
indentBuilder.Append(indentIncreaseCharacter);
indent++;
}
else if (indentSection[i] == indentDecreaseCharacter)
{
indentBuilder.Append(indentDecreaseCharacter);
indent--;
}
else
{
indentBuilder.Append(indentSection[i]);
}
}
// Build the string
string formattedSection = indentBuilder.ToString();
indentBuilder.Length = 0;
// Default caret position
caretPosition = formattedSection.Length - 1;
// Find the caret position
for (int i = formattedSection.Length - 1; i >= 0; i--)
{
if (formattedSection[i] == '\n')
continue;
caretPosition = i;
break;
}
return formattedSection;
}
public static int GetAutoIndentLevel(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;
}
private static void AppendIndentString(int amount)
{
for (int i = 0; i < amount; i++)
indentBuilder.Append("\t");
}
}
}

View File

@ -0,0 +1,552 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System;
using System.Text;
using System.Reflection;
using ExplorerBeta.Input;
using Explorer.UI.Main.Pages.Console.Lexer;
using ExplorerBeta;
namespace Explorer.UI.Main.Pages.Console
{
public class CodeEditor
{
public CodeEditor(TMP_InputField inputField, TextMeshProUGUI inputText, TextMeshProUGUI inputHighlightText, TextMeshProUGUI lineText,
Image background, Image lineHighlight, Image lineNumberBackground, Image scrollbar)
{
this.InputField = inputField;
this.inputText = inputText;
this.inputHighlightText = inputHighlightText;
this.lineText = lineText;
this.background = background;
this.lineHighlight = lineHighlight;
this.lineNumberBackground = lineNumberBackground;
this.scrollbar = scrollbar;
var highlightTextRect = inputHighlightText.GetComponent<RectTransform>();
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = Vector2.zero;
highlightTextRect.offsetMax = Vector2.zero;
if (!AllReferencesAssigned())
{
throw new Exception("CodeEditor: Components are missing!");
}
this.inputTextTransform = inputText.GetComponent<RectTransform>();
this.lineHighlightTransform = lineHighlight.GetComponent<RectTransform>();
ApplyTheme();
ApplyLanguage();
// subscribe to text input changing
InputField.onValueChanged.AddListener(new Action<string>((string s) => { Refresh(); }));
}
private static readonly KeyCode[] lineChangeKeys =
{
KeyCode.Return, KeyCode.Backspace, KeyCode.UpArrow,
KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow
};
private static readonly StringBuilder highlightedBuilder = new StringBuilder(4096);
private static readonly StringBuilder lineBuilder = new StringBuilder();
private readonly InputStringLexer lexer = new InputStringLexer();
private readonly RectTransform inputTextTransform;
private readonly RectTransform lineHighlightTransform;
private string lastText;
private bool lineHighlightLocked;
public readonly TMP_InputField InputField;
private 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 lineNumbers = true;
private int lineNumbersSize = 20;
public int LineCount { get; private set; } = 0;
public int CurrentLine { get; private set; } = 0;
public int CurrentColumn { get; private set; } = 0;
public int CurrentIndent { get; private set; } = 0;
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 string HighlightedText => inputHighlightText.text;
public bool LineNumbers
{
get { return lineNumbers; }
set
{
lineNumbers = value;
//RectTransform inputFieldTransform = InputField.transform as RectTransform;
//RectTransform lineNumberBackgroudTransform = lineNumberBackground.transform as RectTransform;
//// Check for line numbers
//if (lineNumbers == true)
//{
// // Enable line numbers
// lineNumberBackground.gameObject.SetActive(true);
// lineText.gameObject.SetActive(true);
// // Set left value
// inputFieldTransform.offsetMin = new Vector2(lineNumbersSize, inputFieldTransform.offsetMin.y);
// lineNumberBackgroudTransform.sizeDelta = new Vector2(lineNumbersSize + 15, lineNumberBackgroudTransform.sizeDelta.y);
//}
//else
//{
// // Disable line numbers
// lineNumberBackground.gameObject.SetActive(false);
// lineText.gameObject.SetActive(false);
// // Set left value
// inputFieldTransform.offsetMin = new Vector2(0, inputFieldTransform.offsetMin.y);
//}
}
}
// todo maybe not needed
public int LineNumbersSize
{
get { return lineNumbersSize; }
set
{
lineNumbersSize = value;
// Update the line numbers
LineNumbers = lineNumbers;
}
}
public void Update()
{
// Auto indent
if (AutoIndent.autoIndentMode != AutoIndent.IndentMode.None)
{
// Check for new line
if (InputManager.GetKeyDown(KeyCode.Return))
{
AutoIndentCaret();
}
}
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))
{
UpdateCurrentLineHighlight();
}
}
public void Refresh(bool forceUpdate = false)
{
// Trigger a content change event
DisplayedContentChanged(InputField.text, forceUpdate);
}
public void SetLineHighlight(int lineNumber, bool lockLineHighlight)
{
// Check if code editor is not active
if (lineNumber < 1 || lineNumber > LineCount)
return;
//int lineOffset = 0;
//int lineIndex = lineNumber - 1;
// Highlight the current line
lineHighlightTransform.anchoredPosition = new Vector2(5,
(inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight *
-(lineNumber - 1)) - 4f +
inputTextTransform.anchoredPosition.y);
// Lock the line highlight so it cannot be moved
if (lockLineHighlight == true)
LockLineHighlight();
else
UnlockLineHighlight();
}
public void LockLineHighlight()
{
lineHighlightLocked = true;
}
public void UnlockLineHighlight()
{
lineHighlightLocked = false;
}
private void DisplayedContentChanged(string newText, bool forceUpdate)
{
// Update caret position
UpdateCurrentLineColumnIndent();
// Check for change
if ((!forceUpdate && lastText == newText) || string.IsNullOrEmpty(newText))
{
if (string.IsNullOrEmpty(newText))
{
inputHighlightText.text = string.Empty;
}
// Its possible the text was cleared so we need to sync numbers and highlighter
UpdateCurrentLineNumbers();
UpdateCurrentLineHighlight();
return;
}
inputHighlightText.text = SyntaxHighlightContent(newText);
// Sync line numbers and update the line highlight
UpdateCurrentLineNumbers();
UpdateCurrentLineHighlight();
this.lastText = newText;
}
private void UpdateCurrentLineNumbers()
{
// Get the line count
int currentLineCount = inputText.textInfo.lineCount;
int currentLineNumber = 1;
// Check for a change in line
if (currentLineCount != LineCount)
{
try
{
// Update line numbers
lineBuilder.Length = 0;
// Build line numbers string
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;
}
}
}
// Update displayed line numbers
lineText.text = lineBuilder.ToString();
LineCount = currentLineCount;
}
catch { }
}
}
private void UpdateCurrentLineColumnIndent()
{
// Get the current line number
int caret = InputField.caretPosition;
if (caret < 0 || caret >= inputText.textInfo.characterInfo.Count)
{
while (caret >= 0 && caret >= inputText.textInfo.characterInfo.Count)
caret--;
if (caret < 0 || caret >= inputText.textInfo.characterInfo.Count)
{
return;
}
}
CurrentLine = inputText.textInfo.characterInfo[caret].lineNumber;
// Get the total character count
int charCount = 0;
for (int i = 0; i < CurrentLine; i++)
charCount += inputText.textInfo.lineInfo[i].characterCount;
// Get the column position
CurrentColumn = caret - charCount;
CurrentIndent = 0;
// Check for auto indent allowed
if (AutoIndent.allowAutoIndent)
{
for (int i = 0; i < caret && i < InputField.text.Length; i++)
{
char character = InputField.text[i];
// Check for opening indents
if (character == AutoIndent.indentIncreaseCharacter)
CurrentIndent++;
// Check for closing indents
if (character == AutoIndent.indentDecreaseCharacter)
CurrentIndent--;
}
// Dont allow negative indents
if (CurrentIndent < 0)
CurrentIndent = 0;
}
}
private void UpdateCurrentLineHighlight()
{
if (lineHighlightLocked)
return;
try
{
// unity 2018.2 and older may need lineOffset as 0? not sure
//int lineOffset = 1;
int caret = InputField.caretPosition - 1;
var lineHeight = inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight;
var lineNumber = inputText.textInfo.characterInfo[caret].lineNumber;
var 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)
{
if (!InputTheme.allowSyntaxHighlighting)
return inputText;
int offset = 0;
highlightedBuilder.Length = 0;
foreach (var match in lexer.LexInputString(inputText))
{
// Copy text before the match
for (int i = offset; i < match.startIndex; i++)
highlightedBuilder.Append(inputText[i]);
// Add the opening color tag
highlightedBuilder.Append(match.htmlColor);
// Copy text inbetween the match boundaries
for (int i = match.startIndex; i < match.endIndex; i++)
highlightedBuilder.Append(inputText[i]);
// Add the closing color tag
highlightedBuilder.Append(CLOSE_COLOR_TAG);
// Update offset
offset = match.endIndex;
}
// Copy remaining text
for (int i = offset; i < inputText.Length; i++)
highlightedBuilder.Append(inputText[i]);
// Convert to string
inputText = highlightedBuilder.ToString();
return inputText;
}
// todo param is probably pointless
private void AutoIndentCaret()
{
if (CurrentIndent > 0)
{
var indent = GetAutoIndentTab(CurrentIndent);
if (indent.Length > 0)
{
var caretPos = InputField.caretPosition;
var indentMinusOne = indent.Substring(0, indent.Length - 1);
// get last index of {
// check it on the next line if its not already
var text = InputField.text;
var sub = InputField.text.Substring(0, InputField.caretPosition);
var lastIndex = sub.LastIndexOf("{");
var offset = lastIndex - 1;
if (offset >= 0 && text[offset] != '\n' && text[offset] != '\t')
{
var open = "\n" + indentMinusOne;
InputField.text = text.Insert(offset + 1, open);
caretPos += open.Length;
}
// check if should add auto-close }
int numOpen = 0;
int numClose = 0;
char prevChar = default;
foreach (var _char in InputField.text)
{
if (_char == '{')
{
if (prevChar != default && (prevChar == '\\' || prevChar == '{'))
{
if (prevChar == '{')
numOpen--;
}
else
{
numOpen++;
}
}
else if (_char == '}')
{
if (prevChar != default && (prevChar == '\\' || prevChar == '}'))
{
if (prevChar == '}')
numClose--;
}
else
{
numClose++;
}
}
prevChar = _char;
}
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
UpdateCurrentLineColumnIndent();
inputText.text = InputField.text;
inputText.SetText(InputField.text, true);
inputText.Rebuild(CanvasUpdate.Prelayout);
InputField.ForceLabelUpdate();
InputField.Rebuild(CanvasUpdate.Prelayout);
Refresh(true);
}
private string GetAutoIndentTab(int amount)
{
string tab = string.Empty;
for (int i = 0; i < amount; i++)
tab += "\t";
return tab;
}
private void ApplyTheme()
{
// Check for missing references
if (!AllReferencesAssigned())
throw new Exception("Cannot apply theme because one or more required component references are missing. ");
// Apply theme colors
InputField.caretColor = InputTheme.caretColor;
inputText.color = InputTheme.textColor;
inputHighlightText.color = InputTheme.textColor;
background.color = InputTheme.backgroundColor;
lineHighlight.color = InputTheme.lineHighlightColor;
lineNumberBackground.color = InputTheme.lineNumberBackgroundColor;
lineText.color = InputTheme.lineNumberTextColor;
scrollbar.color = InputTheme.scrollbarColor;
}
private void ApplyLanguage()
{
lexer.UseMatchers(CodeTheme.DelimiterSymbols, CodeTheme.Matchers);
}
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,164 @@
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Explorer.UI.Main.Pages.Console.Lexer;
using System.Runtime.InteropServices;
namespace Explorer.UI.Main.Pages.Console
{
public static class CodeTheme
{
internal static readonly StringBuilder sharedBuilder = new StringBuilder();
private static char[] delimiterSymbolCache = null;
private static MatchLexer[] matchers = null;
public static string languageName = "C#";
public static string delimiterSymbols = "[ ] ( ) { } ; : , .";
public static KeywordGroupMatch[] keywordGroups = new KeywordGroupMatch[]
{
// VALID KEYWORDS
new KeywordGroupMatch()
{
highlightColor = new Color(0.33f, 0.61f, 0.83f, 1.0f),
caseSensitive = true,
keywords = @"add as ascending await base bool break by byte
case catch char checked const continue decimal default descending do dynamic
else enum equals false finally fixed float for foreach from global goto group
if in int into is join let lock long new null object on orderby out params
partial ref remove return sbyte sealed select short sizeof stackalloc string
struct switch this throw true try typeof uint ulong unchecked unsafe ushort
using value var void where while yield"
},
// INVALID KEYWORDS (cannot use inside method scope)
new KeywordGroupMatch()
{
highlightColor = new Color(0.95f, 0.10f, 0.10f, 1.0f),
caseSensitive = true,
keywords = @"abstract async class delegate explicit extern get
implicit interface internal namespace operator override private protected public
readonly set static virtual volatile"
}
};
/// <summary>
/// A symbol group used to specify which symbols should be highlighted.
/// </summary>
public static SymbolGroupMatch symbolGroup = new SymbolGroupMatch
{
symbols = @"[ ] ( ) . ? : + - * / % & | ^ ~ = < > ++ -- && || << >> == != <= >=
+= -= *= /= %= &= |= ^= <<= >>= -> ?? =>",
highlightColor = new Color(0.58f, 0.47f, 0.37f, 1.0f),
};
/// <summary>
/// A number group used to specify whether numbers should be highlighted.
/// </summary>
public static NumberGroupMatch numberGroup = new NumberGroupMatch
{
highlightNumbers = true,
highlightColor = new Color(0.58f, 0.33f, 0.33f, 1.0f)
};
/// <summary>
/// A comment group used to specify which strings open and close comments.
/// </summary>
public static CommentGroupMatch commentGroup = new CommentGroupMatch
{
blockCommentEnd = @"*/",
blockCommentStart = @"/*",
lineCommentStart = @"//",
lineCommentHasPresedence = true,
highlightColor = new Color(0.34f, 0.65f, 0.29f, 1.0f),
};
/// <summary>
/// A literal group used to specify whether quote strings should be highlighted.
/// </summary>
public static LiteralGroupMatch literalGroup = new LiteralGroupMatch
{
highlightLiterals = true,
highlightColor = new Color(0.79f, 0.52f, 0.32f, 1.0f)
};
///// <summary>
///// Options group for all auto indent related settings.
///// </summary>
//public static AutoIndent autoIndent;
// Properties
internal static char[] DelimiterSymbols
{
get
{
if (delimiterSymbolCache == null)
{
// Split by space
string[] symbols = delimiterSymbols.Split(' ');
int count = 0;
// Count the number of valid symbols
for (int i = 0; i < symbols.Length; i++)
if (symbols[i].Length == 1)
count++;
// Allocate array
delimiterSymbolCache = new char[count];
// Copy symbols
for (int i = 0, index = 0; i < symbols.Length; i++)
{
// Require only 1 character
if (symbols[i].Length == 1)
{
// Get the first character for the string
delimiterSymbolCache[index] = symbols[i][0];
index++;
}
}
}
return delimiterSymbolCache;
}
}
internal static MatchLexer[] Matchers
{
get
{
if (matchers == null)
{
List<MatchLexer> matcherList = new List<MatchLexer>
{
commentGroup,
symbolGroup,
numberGroup,
literalGroup
};
matcherList.AddRange(keywordGroups);
matchers = matcherList.ToArray();
}
return matchers;
}
}
// Methods
internal static void Invalidate()
{
foreach (KeywordGroupMatch group in keywordGroups)
group.Invalidate();
symbolGroup.Invalidate();
commentGroup.Invalidate();
numberGroup.Invalidate();
literalGroup.Invalidate();
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console
{
public static class InputTheme
{
public static bool allowSyntaxHighlighting = true;
public static Color caretColor = new Color32(255, 255, 255, 255);
public static Color textColor = new Color32(255, 255, 255, 255);
public static Color backgroundColor = new Color32(37, 37, 37, 255);
public static Color lineHighlightColor = new Color32(50, 50, 50, 255);
public static Color lineNumberBackgroundColor = new Color32(25, 25, 25, 255);
public static Color lineNumberTextColor = new Color32(180, 180, 180, 255);
public static Color scrollbarColor = new Color32(45, 50, 50, 255);
}
}

View File

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using Explorer.Unstrip.ColorUtility;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
/// <summary>
/// Used to match line and block comments.
/// </summary>
public sealed class CommentGroupMatch : MatchLexer
{
[NonSerialized]
private string htmlColor = null;
// Public
/// <summary>
/// The string that denotes the start of a line comment.
/// Leave this value empty if line comments should not be highlighted.
/// </summary>
public string lineCommentStart;
/// <summary>
/// The string that denotes the start of a block comment.
/// Leave this value empty if block comments should not be highlighted.
/// </summary>
public string blockCommentStart;
/// <summary>
/// The string that denotes the end of a block comment.
/// </summary>
public string blockCommentEnd;
/// <summary>
/// The color that comments will be highlighted with.
/// </summary>
public Color highlightColor = Color.black;
public bool lineCommentHasPresedence = true;
// Properties
/// <summary>
/// Retrusn a value indicating whether any comment highlighting is enabled.
/// A valid line or block comment start string must be specified in order for comment highlighting to be enabled.
/// </summary>
public bool HasCommentHighlighting
{
get
{
return string.IsNullOrEmpty(lineCommentStart) == false ||
string.IsNullOrEmpty(blockCommentStart) == false;
}
}
/// <summary>
/// Get the html tag color that comments will be highlighted with.
/// </summary>
public override string HTMLColor
{
get
{
// Build html color string
if (htmlColor == null)
htmlColor = "<#" + highlightColor.ToHex() + ">";
return htmlColor;
}
}
/// <summary>
/// Returns an enumerable collection of characters from this group that can act as delimiter symbols when they appear after a keyword.
/// </summary>
public override IEnumerable<char> SpecialStartCharacters
{
get
{
if (string.IsNullOrEmpty(lineCommentStart) == false)
yield return lineCommentStart[0];
if (string.IsNullOrEmpty(blockCommentEnd) == false)
yield return blockCommentEnd[0];
}
}
/// <summary>
/// Returns an enumerable collection of characters from this group that can act as delimiter symbols when they appear before a keyword.
/// </summary>
public override IEnumerable<char> SpecialEndCharacters
{
get
{
if (string.IsNullOrEmpty(blockCommentEnd) == false)
yield return blockCommentEnd[blockCommentEnd.Length - 1];
}
}
// Methods
/// <summary>
/// Causes the cached values to be reloaded.
/// Useful for editor visualisation.
/// </summary>
public override void Invalidate()
{
this.htmlColor = null;
}
/// <summary>
/// Returns true if the lexer input contains a valid comment format as the next character sequence.
/// </summary>
/// <param name="lexer">The input lexer</param>
/// <returns>True if a comment was found or false if not</returns>
public override bool IsImplicitMatch(ILexer lexer)
{
if (lineCommentHasPresedence == true)
{
// Parse line comments then block comments
if (IsLineCommentMatch(lexer) == true ||
IsBlockCommentMatch(lexer) == true)
return true;
}
else
{
// Parse block comments then line coments
if (IsBlockCommentMatch(lexer) == true ||
IsLineCommentMatch(lexer) == true)
return true;
}
// Not a comment
return false;
}
private bool IsLineCommentMatch(ILexer lexer)
{
// Check for line comment
if (string.IsNullOrEmpty(lineCommentStart) == false)
{
lexer.Rollback();
bool match = true;
for (int i = 0; i < lineCommentStart.Length; i++)
{
if (lineCommentStart[i] != lexer.ReadNext())
{
match = false;
break;
}
}
// Check for valid match
if (match == true)
{
// Read until end
while (IsEndLineOrEndFile(lexer, lexer.ReadNext()) == false) ;
// Matched a single line comment
return true;
}
}
return false;
}
private bool IsBlockCommentMatch(ILexer lexer)
{
// Check for block comment
if (string.IsNullOrEmpty(blockCommentStart) == false)
{
lexer.Rollback();
bool match = true;
for (int i = 0; i < blockCommentStart.Length; i++)
{
if (blockCommentStart[i] != lexer.ReadNext())
{
match = false;
break;
}
}
// Check for valid match
if (match == true)
{
// Read until end or closing block
while (IsEndLineOrString(lexer, blockCommentEnd) == false) ;
// Matched a multi-line block commend
return true;
}
}
return false;
}
private bool IsEndLineOrEndFile(ILexer lexer, char character)
{
if (lexer.EndOfStream == true ||
character == '\n' ||
character == '\r')
{
// Line end or file end
return true;
}
return false;
}
private bool IsEndLineOrString(ILexer lexer, string endString)
{
for (int i = 0; i < endString.Length; i++)
{
// Check for end of stream
if (lexer.EndOfStream == true)
return true;
// Check for matching end string
if (endString[i] != lexer.ReadNext())
return false;
}
// We matched the end string
return true;
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
// Types
/// <summary>
/// Represents a keyword position where a special character may appear.
/// </summary>
public enum SpecialCharacterPosition
{
/// <summary>
/// The special character may appear before a keyword.
/// </summary>
Start,
/// <summary>
/// The special character may appear after a keyword.
/// </summary>
End,
};
/// <summary>
/// Represents a streamable lexer input which can be examined by matchers.
/// </summary>
public interface ILexer
{
// Properties
/// <summary>
/// Returns true if there are no more characters to read.
/// </summary>
bool EndOfStream { get; }
/// <summary>
/// Returns the previously read character or '\0' if there is no previous character.
/// </summary>
char Previous { get; }
// Methods
/// <summary>
/// Attempt to read the next character.
/// </summary>
/// <returns>The next character in the stream or '\0' if the end of stream is reached</returns>
char ReadNext();
/// <summary>
/// Causes the lexer to return its state to a previously commited state.
/// </summary>
/// <param name="amount">Use -1 to return to the last commited state or a positive number to represent the number of characters to rollback</param>
void Rollback(int amount = -1);
/// <summary>
/// Causes all read characters to be commited meaning that rollback will return to this lexer state.
/// </summary>
void Commit();
/// <summary>
/// Determines whether the specified character is considered a special symbol by the lexer meaning that it is able to act as a delimiter.
/// </summary>
/// <param name="character">The character to check</param>
/// <param name="position">The character position to check. This determines whether the character may appear at the start or end of a keyword</param>
/// <returns>True if the character is a valid delimiter or false if not</returns>
bool IsSpecialSymbol(char character, SpecialCharacterPosition position = SpecialCharacterPosition.Start);
}
}

View File

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
internal struct InputStringMatchInfo
{
// Public
public int startIndex;
public int endIndex;
public string htmlColor;
}
internal class InputStringLexer : ILexer
{
// Private
private string inputString = null;
private MatchLexer[] matchers = null;
private readonly HashSet<char> specialStartSymbols = new HashSet<char>();
private readonly HashSet<char> specialEndSymbols = new HashSet<char>();
private char current = ' ';
private char previous = ' ';
private int currentIndex = 0;
private int currentLookaheadIndex = 0;
// Properties
public bool EndOfStream
{
get { return currentLookaheadIndex >= inputString.Length; }
}
public char Previous
{
get { return previous; }
}
// Methods
public void UseMatchers(char[] delimiters, MatchLexer[] matchers)
{
// Store matchers
this.matchers = matchers;
// Clear old symbols
specialStartSymbols.Clear();
specialEndSymbols.Clear();
// Check for any delimiter characters
if (delimiters != null)
{
// Add delimiters
foreach (char character in delimiters)
{
// Add to start
if (specialStartSymbols.Contains(character) == false)
specialStartSymbols.Add(character);
// Add to end
if (specialEndSymbols.Contains(character) == false)
specialEndSymbols.Add(character);
}
}
// Check for any matchers
if (matchers != null)
{
// Add all special symbols which can act as a delimiter
foreach (MatchLexer lexer in matchers)
{
foreach (char special in lexer.SpecialStartCharacters)
if (specialStartSymbols.Contains(special) == false)
specialStartSymbols.Add(special);
foreach (char special in lexer.SpecialEndCharacters)
if (specialEndSymbols.Contains(special) == false)
specialEndSymbols.Add(special);
}
}
}
public IEnumerable<InputStringMatchInfo> LexInputString(string input)
{
if (input == null || matchers == null || matchers.Length == 0)
yield break;
// Store the input string
this.inputString = input;
this.current = ' ';
this.previous = ' ';
this.currentIndex = 0;
this.currentLookaheadIndex = 0;
// Process the input string
while (EndOfStream == false)
{
bool didMatchLexer = false;
// Read any white spaces
ReadWhiteSpace();
// Process each matcher
foreach (MatchLexer matcher in matchers)
{
// Get the current index
int startIndex = currentIndex;
// Try to match
bool isMatched = matcher.IsMatch(this);
if (isMatched == true)
{
// Get the end index of the match
int endIndex = currentIndex;
// Set matched flag
didMatchLexer = true;
// Register the match
yield return new InputStringMatchInfo
{
startIndex = startIndex,
endIndex = endIndex,
htmlColor = matcher.HTMLColor,
};
// Move to next character
break;
}
}
if (didMatchLexer == false)
{
// Move to next
ReadNext();
Commit();
}
}
}
public char ReadNext()
{
// Check for end of stream
if (EndOfStream == true)
return '\0';
// Update previous character
previous = current;
// Get the character
current = inputString[currentLookaheadIndex];
currentLookaheadIndex++;
return current;
}
public void Rollback(int amount = -1)
{
if (amount == -1)
{
// Revert to index
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()
{
// Read until white space
while (char.IsWhiteSpace(ReadNext()) == true)
{
// Consume the char
Commit();
}
// Return lexer state
Rollback();
}
}
}

View File

@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Explorer.Unstrip.ColorUtility;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
/// <summary>
/// A matcher that checks for a number of predefined keywords in the lexer stream.
/// </summary>
public sealed class KeywordGroupMatch : MatchLexer
{
// Private
private static readonly HashSet<string> shortlist = new HashSet<string>();
private static readonly Stack<string> removeList = new Stack<string>();
private string[] keywordCache = null;
private string htmlColor = null;
// Public
/// <summary>
/// Used for editor gui only. Has no purpose other than to give the inspector foldout a nice name
/// </summary>
public string group = "Keyword Group"; // This value is not used - it just gives the inspector foldout a nice name
/// <summary>
/// A string containing one or more keywords separated by a space character that will be used by this matcher.
/// </summary>
public string keywords;
/// <summary>
/// The color that any matched keywords will be highlighted.
/// </summary>
public Color highlightColor = Color.black;
/// <summary>
/// Should keyword matching be case sensitive.
/// </summary>
public bool caseSensitive = true;
// Properties
/// <summary>
/// Get a value indicating whether keyword highlighting is enabled based upon the number of valid keywords found.
/// </summary>
public bool HasKeywordHighlighting
{
get
{
// Check for valid keyword
if (string.IsNullOrEmpty(keywords) == false)
return true;
return false;
}
}
/// <summary>
/// Get the html formatted color tag that any matched keywords will be highlighted with.
/// </summary>
public override string HTMLColor
{
get
{
// Get html color
if (htmlColor == null)
htmlColor = "<#" + highlightColor.ToHex() + ">";
return htmlColor;
}
}
// Methods
/// <summary>
/// Causes any cached data to be reloaded.
/// </summary>
public override void Invalidate()
{
this.htmlColor = null;
}
/// <summary>
/// Check whether the specified lexer has a valid keyword at its current position.
/// </summary>
/// <param name="lexer">The input lexer to check</param>
/// <returns>True if the stream has a keyword or false if not</returns>
public override bool IsImplicitMatch(ILexer lexer)
{
// Make sure cache is built
BuildKeywordCache();
// Require whitespace before character
if (char.IsWhiteSpace(lexer.Previous) == false &&
lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End) == false)
return false;
// Clear old data
shortlist.Clear();
// Read the first character
int currentIndex = 0;
char currentChar = lexer.ReadNext();
// Add to shortlist
for (int i = 0; i < keywordCache.Length; i++)
if (CompareChar(keywordCache[i][0], currentChar) == true)
shortlist.Add(keywordCache[i]);
// Check for no matches we can skip the heavy work quickly
if (shortlist.Count == 0)
return false;
do
{
// Check for end of stream
if (lexer.EndOfStream == true)
{
RemoveLongStrings(currentIndex + 1);
break;
}
// Read next character
currentChar = lexer.ReadNext();
currentIndex++;
// Check for end of word
if (char.IsWhiteSpace(currentChar) == true ||
lexer.IsSpecialSymbol(currentChar, SpecialCharacterPosition.Start) == true)
{
// Finalize any matching candidates and undo the reading of the space or special character
RemoveLongStrings(currentIndex);
lexer.Rollback(1);
break;
}
// Check for shortlist match
foreach (string keyword in shortlist)
{
if (currentIndex >= keyword.Length ||
CompareChar(keyword[currentIndex], currentChar) == false)
{
removeList.Push(keyword);
}
}
// Remove from short list
while (removeList.Count > 0)
shortlist.Remove(removeList.Pop());
}
while (shortlist.Count > 0);
// Check for any words in the shortlist
return shortlist.Count > 0;
}
private void RemoveLongStrings(int length)
{
foreach (string keyword in shortlist)
{
if (keyword.Length > length)
{
removeList.Push(keyword);
}
}
// Remove from short list
while (removeList.Count > 0)
shortlist.Remove(removeList.Pop());
}
private void BuildKeywordCache()
{
// Check if we need to build the cache
if (keywordCache == null)
{
// Get keyowrds and insert them into a cache array for quick reference
var kwSplit = keywords.Split(' ');
var list = new List<string>();
foreach (var kw in kwSplit)
{
if (!string.IsNullOrEmpty(kw) && kw.Length > 0)
{
list.Add(kw);
}
}
keywordCache = list.ToArray();
}
}
private bool CompareChar(char a, char b)
{
// Check for direct match
if (a == b)
return true;
// Check for case sensitive
if (caseSensitive == false)
{
if (char.ToUpper(a, CultureInfo.CurrentCulture) ==
char.ToUpper(b, CultureInfo.CurrentCulture))
{
// Check for match ignoring case
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Explorer.Unstrip.ColorUtility;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
/// <summary>
/// A matcher that checks for quote strings in the lexer stream.
/// </summary>
[Serializable]
public sealed class LiteralGroupMatch : MatchLexer
{
// Private
private string htmlColor = null;
// Public
/// <summary>
/// Should literal be highlighted.
/// When true, any text surrounded by double quotes will be highlighted.
/// </summary>
public bool highlightLiterals = true;
/// <summary>
/// The color that any matched literals will be highlighted.
/// </summary>
public Color highlightColor = Color.black;
// Properties
/// <summary>
/// Get a value indicating whether literal highlighting is enabled.
/// </summary>
public bool HasLiteralHighlighting
{
get { return highlightLiterals; }
}
/// <summary>
/// Get the html formatted color tag that any matched literals will be highlighted with.
/// </summary>
public override string HTMLColor
{
get
{
if (htmlColor == null)
htmlColor = "<#" + highlightColor.ToHex() + ">";
return htmlColor;
}
}
/// <summary>
/// Returns special symbols that can act as delimiters when appearing before a word.
/// In this case '"' will be returned.
/// </summary>
public override IEnumerable<char> SpecialStartCharacters
{
get
{
yield return '"';
}
}
/// <summary>
/// Returns special symbols that can act as delimiters when appearing after a word.
/// In this case '"' will be returned.
/// </summary>
public override IEnumerable<char> SpecialEndCharacters
{
get
{
yield return '"';
}
}
// Methods
/// <summary>
/// Causes any cached data to be reloaded.
/// </summary>
public override void Invalidate()
{
this.htmlColor = null;
}
/// <summary>
/// Check whether the specified lexer has a valid literal at its current position.
/// </summary>
/// <param name="lexer">The input lexer to check</param>
/// <returns>True if the stream has a literal or false if not</returns>
public override bool IsImplicitMatch(ILexer lexer)
{
// Skip highlighting
if (highlightLiterals == false)
return false;
// Check for quote
if (lexer.ReadNext() == '"')
{
// Read all characters inside the quote
while (IsClosingQuoteOrEndFile(lexer, lexer.ReadNext()) == false) ;
// Found a valid literal
return true;
}
return false;
}
private bool IsClosingQuoteOrEndFile(ILexer lexer, char character)
{
if (lexer.EndOfStream == true ||
character == '"')
{
// We have found the end of the file or quote
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
public abstract class MatchLexer
{
/// <summary>
/// Get the html formatted color tag that any matched text will be highlighted with.
/// </summary>
public abstract string HTMLColor { get; }
/// <summary>
/// Get an enumerable collection of special characters that can act as delimiter symbols when they appear before a word.
/// </summary>
public virtual IEnumerable<char> SpecialStartCharacters { get { yield break; } }
/// <summary>
/// Get an enumerable collection of special characters that can act as delimiter symbols when they appear after a word.
/// </summary>
public virtual IEnumerable<char> SpecialEndCharacters { get { yield break; } }
// Methods
/// <summary>
/// Checks the specified lexers current position for a certain sequence of characters as defined by the inheriting matcher.
/// </summary>
/// <param name="lexer"></param>
/// <returns></returns>
public abstract bool IsImplicitMatch(ILexer lexer);
/// <summary>
/// Causes the matcher to invalidate any cached data forcing it to be regenerated or reloaded.
/// </summary>
public virtual void Invalidate() { }
/// <summary>
/// Attempts to check for a match in the specified lexer.
/// </summary>
/// <param name="lexer">The lexer that will be checked</param>
/// <returns>True if a match was found or false if not</returns>
public bool IsMatch(ILexer lexer)
{
// Check for implicit match
bool match = IsImplicitMatch(lexer);
if (match == true)
{
// Consume read tokens
lexer.Commit();
}
else
{
// Revert lexer state
lexer.Rollback();
}
return match;
}
}
}

View File

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Explorer.Unstrip.ColorUtility;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
/// <summary>
/// A matcher that checks for any numbers that appear in the lexer stream.
/// </summary>
public sealed class NumberGroupMatch : MatchLexer
{
// Private
private string htmlColor = null;
// Public
/// <summary>
/// Should number highlighting be used.
/// When false, numbers will appear in the default text color as defined by the current editor theme.
/// </summary>
public bool highlightNumbers = true;
/// <summary>
/// The color that any matched numbers will be highlighted.
/// </summary>
public Color highlightColor = Color.black;
// Properties
/// <summary>
/// Get a value indicating whether keyword highlighting is enabled.
/// </summary>
public bool HasNumberHighlighting
{
get { return highlightNumbers; }
}
/// <summary>
/// Get the html formatted color tag that any matched numbers will be highlighted with.
/// </summary>
public override string HTMLColor
{
get
{
if (htmlColor == null)
htmlColor = "<#" + highlightColor.ToHex() + ">";
return htmlColor;
}
}
// Methods
/// <summary>
/// Causes any cached data to be reloaded.
/// </summary>
public override void Invalidate()
{
this.htmlColor = null;
}
/// <summary>
/// Check whether the specified lexer has a valid number sequence at its current position.
/// </summary>
/// <param name="lexer">The input lexer to check</param>
/// <returns>True if the stream has a number sequence or false if not</returns>
public override bool IsImplicitMatch(ILexer lexer)
{
// Skip highlighting
if (highlightNumbers == false)
return false;
// Require whitespace or symbols before numbers
if (char.IsWhiteSpace(lexer.Previous) == false &&
lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End) == false)
{
// There is some other character before the potential number
return false;
}
bool matchedNumber = false;
// Consume the number characters
while (lexer.EndOfStream == false)
{
// Check for valid numerical character
if (IsNumberOrDecimalPoint(lexer.ReadNext()) == true)
{
// We have found a number or decimal
matchedNumber = true;
lexer.Commit();
}
else
{
lexer.Rollback();
break;
}
}
return matchedNumber;
}
private bool IsNumberOrDecimalPoint(char character) => char.IsNumber(character) || character == '.';
}
}

View File

@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Explorer.Unstrip.ColorUtility;
using ExplorerBeta;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
/// <summary>
/// A matcher that checks for a number of predefined symbols in the lexer stream.
/// </summary>
[Serializable]
public sealed class SymbolGroupMatch : MatchLexer
{
// Private
private static readonly List<string> shortlist = new List<string>();
private static readonly Stack<string> removeList = new Stack<string>();
[NonSerialized]
private string[] symbolCache = null;
[NonSerialized]
private string htmlColor = null;
// Public
/// <summary>
/// A string containing one or more symbols separated by a space character that will be used by the matcher.
/// Symbols can be one or more characters long and should not contain numbers or letters.
/// </summary>
public string symbols;
/// <summary>
/// The color that any matched symbols will be highlighted.
/// </summary>
public Color highlightColor = Color.black;
// Properties
/// <summary>
/// Get a value indicating whether symbol highlighting is enabled based upon the number of valid symbols found.
/// </summary>
public bool HasSymbolHighlighting
{
get { return symbols.Length > 0; }
}
/// <summary>
/// Get the html formatted color tag that any matched symbols will be highlighted with.
/// </summary>
public override string HTMLColor
{
get
{
// Get html color
if (htmlColor == null)
htmlColor = "<#" + highlightColor.ToHex() + ">";
return htmlColor;
}
}
/// <summary>
/// Returns special symbols that can act as delimiters when appearing before a word.
/// </summary>
public override IEnumerable<char> SpecialStartCharacters
{
get
{
// Make sure cahce is created
BuildSymbolCache();
// Get the first character of each symbol
foreach (string symbol in symbolCache.Where(x => x.Length > 0))
yield return symbol[0];
}
}
/// <summary>
/// Returns special symbols that can act as delimiters when appearing after a word.
/// In this case '"' will be returned.
/// </summary>
public override IEnumerable<char> SpecialEndCharacters
{
get
{
// Make sure cahce is created
BuildSymbolCache();
// Get the first character of each symbol
foreach (string symbol in symbolCache.Where(x => x.Length > 0))
yield return symbol[0];
}
}
// Methods
/// <summary>
/// Causes any cached data to be reloaded.
/// </summary>
public override void Invalidate()
{
this.htmlColor = null;
}
/// <summary>
/// Checks whether the specified lexer has a valid symbol at its current posiiton.
/// </summary>
/// <param name="lexer">The input lexer to check</param>
/// <returns>True if the stream has a symbol or false if not</returns>
public override bool IsImplicitMatch(ILexer lexer)
{
// Make sure cache is created
BuildSymbolCache();
if (lexer == null)
{
return false;
}
// Require whitespace, letter or digit before symbol
if (char.IsWhiteSpace(lexer.Previous) == false &&
char.IsLetter(lexer.Previous) == false &&
char.IsDigit(lexer.Previous) == false &&
lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End) == false)
return false;
// Clear old data
shortlist.Clear();
// Read the first character
int currentIndex = 0;
char currentChar = lexer.ReadNext();
// Add to shortlist
for (int i = symbolCache.Length - 1; i >= 0; i--)
{
if (symbolCache[i][0] == currentChar)
shortlist.Add(symbolCache[i]);
}
// No potential matches
if (shortlist.Count == 0)
return false;
do
{
// Check for end of stream
if (lexer.EndOfStream == true)
{
RemoveLongStrings(currentIndex + 1);
break;
}
// Read next character
currentChar = lexer.ReadNext();
currentIndex++;
if (char.IsWhiteSpace(currentChar) == true ||
char.IsLetter(currentChar) == true ||
char.IsDigit(currentChar) == true ||
lexer.IsSpecialSymbol(currentChar, SpecialCharacterPosition.Start) == true)
{
RemoveLongStrings(currentIndex);
lexer.Rollback(1);
break;
}
// Check for shortlist match
foreach (string symbol in shortlist)
{
if (currentIndex >= symbol.Length ||
symbol[currentIndex] != currentChar)
{
removeList.Push(symbol);
}
}
// Remove from short list
while (removeList.Count > 0)
shortlist.Remove(removeList.Pop());
}
while (shortlist.Count > 0);
// Check for any words in the shortlist
return shortlist.Count > 0;
}
private void RemoveLongStrings(int length)
{
foreach (string keyword in shortlist)
{
if (keyword.Length > length)
{
removeList.Push(keyword);
}
}
// Remove from short list
while (removeList.Count > 0)
shortlist.Remove(removeList.Pop());
}
private void BuildSymbolCache()
{
if (string.IsNullOrEmpty(symbols))
{
ExplorerCore.LogWarning("Symbol cache is null!");
symbolCache = new string[0];
}
else
{
// Get symbols and insert them into a cache array for quick reference
var symSplit = symbols.Split(' ');
var list = new List<string>();
foreach (var sym in symSplit)
{
if (!string.IsNullOrEmpty(sym) && sym.Length > 0)
{
list.Add(sym);
}
}
symbolCache = list.ToArray();
}
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using ExplorerBeta;
using UnityEngine;
// Thanks to ManlyMarco for this
namespace Explorer.UI.Main.Pages.Console
{
public struct AutoComplete
{
public string Full => Prefix + Addition;
public readonly string Prefix;
public readonly string Addition;
public readonly Contexts Context;
public Color TextColor => Context == Contexts.Namespace
? Color.gray
: Color.white;
public AutoComplete(string addition, string prefix, Contexts type)
{
Addition = addition;
Prefix = prefix;
Context = type;
}
public enum Contexts
{
Namespace,
Other
}
}
public static class AutoCompleteHelpers
{
public static HashSet<string> Namespaces => _namespaces ?? GetNamespaces();
private static HashSet<string> _namespaces;
private static HashSet<string> GetNamespaces()
{
var set = new HashSet<string>(
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(GetTypes)
.Where(x => x.IsPublic && !string.IsNullOrEmpty(x.Namespace))
.Select(x => x.Namespace));
return _namespaces = set;
IEnumerable<Type> GetTypes(Assembly asm) => asm.TryGetTypes();
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Mono.CSharp;
// Thanks to ManlyMarco for this
namespace Explorer.UI.Main.Pages.Console
{
internal 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 (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
string name = assembly.GetName().Name;
if (StdLib.Contains(name))
continue;
import(assembly);
}
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Mono.CSharp;
using UnityEngine;
using ExplorerBeta;
namespace Explorer.UI.Main
{
public class ScriptInteraction : InteractiveBase
{
public static void Log(object message)
{
ExplorerCore.Log(message);
}
public static object CurrentTarget()
{
throw new NotImplementedException("TODO");
}
public static object[] AllTargets()
{
throw new NotImplementedException("TODO");
}
public static void Inspect(object obj)
{
throw new NotImplementedException("TODO");
}
public static void Inspect(Type type)
{
throw new NotImplementedException("TODO");
}
public static void Help()
{
ExplorerCore.Log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
ExplorerCore.Log(" C# Console Help ");
ExplorerCore.Log("");
ExplorerCore.Log("The following helper methods are available:");
ExplorerCore.Log("");
ExplorerCore.Log("void Log(object message)");
ExplorerCore.Log(" prints a message to the console window and debug log");
ExplorerCore.Log(" usage: Log(\"hello world\");");
ExplorerCore.Log("");
ExplorerCore.Log("object CurrentTarget()");
ExplorerCore.Log(" returns the target object of the current tab (in tab view mode only)");
ExplorerCore.Log(" usage: var target = CurrentTarget();");
ExplorerCore.Log("");
ExplorerCore.Log("object[] AllTargets()");
ExplorerCore.Log(" returns an object[] array containing all currently inspected objects");
ExplorerCore.Log(" usage: var targets = AllTargets();");
ExplorerCore.Log("");
ExplorerCore.Log("void Inspect(object obj)");
ExplorerCore.Log(" inspects the provided object in a new window.");
ExplorerCore.Log(" usage: Inspect(Camera.main);");
ExplorerCore.Log("");
ExplorerCore.Log("void Inspect(Type type)");
ExplorerCore.Log(" attempts to inspect the provided type with static-only reflection.");
ExplorerCore.Log(" usage: Inspect(typeof(Camera));");
}
}
}

View File

@ -1,7 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Explorer.UI.Main.Pages.Console;
using ExplorerBeta;
using ExplorerBeta.UI;
using ExplorerBeta.UI.Main;
using ExplorerBeta.Unstrip.Resources;
using TMPro;
using UnhollowerRuntimeLib;
using UnityEngine;
using UnityEngine.UI;
namespace Explorer.UI.Main.Pages
{
@ -9,14 +19,350 @@ namespace Explorer.UI.Main.Pages
{
public override string Name => "C# Console";
public static ConsolePage Instance { get; private set; }
private CodeEditor codeEditor;
private ScriptEvaluator m_evaluator;
public static List<AutoComplete> AutoCompletes = new List<AutoComplete>();
public static List<string> UsingDirectives;
public static readonly string[] DefaultUsing = new string[]
{
"System",
"UnityEngine",
"System.Linq",
"System.Collections",
"System.Collections.Generic",
"System.Reflection"
};
public override void Init()
{
Instance = this;
try
{
ResetConsole();
foreach (var use in DefaultUsing)
{
AddUsing(use);
}
ConstructUI();
}
catch (Exception e)
{
ExplorerCore.LogWarning($"Error setting up console!\r\nMessage: {e.Message}");
// TODO
}
}
public override void Update()
{
codeEditor?.Update();
}
internal string AsmToUsing(string asm, bool richText = false)
{
if (richText)
{
return $"<color=#569cd6>using</color> {asm};";
}
return $"using {asm};";
}
public void AddUsing(string asm)
{
if (!UsingDirectives.Contains(asm))
{
UsingDirectives.Add(asm);
Evaluate(AsmToUsing(asm), true);
}
}
public object Evaluate(string str, bool suppressWarning = false)
{
object ret = VoidType.Value;
m_evaluator.Compile(str, out var compiled);
try
{
if (compiled == null)
{
throw new Exception("Mono.Csharp Service was unable to compile the code provided.");
}
compiled.Invoke(ref ret);
}
catch (Exception e)
{
if (!suppressWarning)
{
ExplorerCore.LogWarning(e.GetType() + ", " + e.Message);
}
}
return ret;
}
public void ResetConsole()
{
if (m_evaluator != null)
{
m_evaluator.Dispose();
}
m_evaluator = new ScriptEvaluator(new StringWriter(new StringBuilder())) { InteractiveBaseClass = typeof(ScriptInteraction) };
UsingDirectives = new List<string>();
}
#region UI Construction
public void ConstructUI()
{
Content = UIFactory.CreateUIObject("C# Console", MainMenu.Instance.PageViewport);
var mainLayout = Content.AddComponent<LayoutElement>();
mainLayout.preferredHeight = 300;
mainLayout.flexibleHeight = 4;
var mainGroup = Content.AddComponent<VerticalLayoutGroup>();
mainGroup.childControlHeight = true;
mainGroup.childControlWidth = true;
mainGroup.childForceExpandHeight = true;
mainGroup.childForceExpandWidth = true;
var topBarObj = UIFactory.CreateHorizontalGroup(Content);
var topBarLayout = topBarObj.AddComponent<LayoutElement>();
topBarLayout.preferredHeight = 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;
var topBarLabel = UIFactory.CreateLabel(topBarObj, TextAnchor.MiddleLeft);
var topBarLabelLayout = topBarLabel.AddComponent<LayoutElement>();
topBarLabelLayout.preferredWidth = 800;
topBarLabelLayout.flexibleWidth = 10;
var topBarText = topBarLabel.GetComponent<Text>();
topBarText.text = "C# Console";
topBarText.fontSize = 20;
var compileBtnObj = UIFactory.CreateButton(topBarObj);
var compileBtnLayout = compileBtnObj.AddComponent<LayoutElement>();
compileBtnLayout.preferredWidth = 80;
compileBtnLayout.flexibleWidth = 0;
var compileButton = compileBtnObj.GetComponent<Button>();
var compileBtnColors = compileButton.colors;
compileBtnColors.normalColor = new Color(14f/255f, 106f/255f, 14f/255f);
compileButton.colors = compileBtnColors;
var btnText = compileBtnObj.GetComponentInChildren<Text>();
btnText.text = ">";
btnText.fontSize = 25;
btnText.color = Color.white;
var consoleBase = UIFactory.CreateUIObject("CodeEditor", Content);
var consoleLayout = consoleBase.AddComponent<LayoutElement>();
consoleLayout.preferredHeight = 500;
consoleLayout.flexibleHeight = 5;
consoleBase.AddComponent<RectMask2D>();
var mainRect = consoleBase.GetComponent<RectTransform>();
mainRect.pivot = Vector2.one * 0.5f;
mainRect.anchorMin = Vector2.zero;
mainRect.anchorMax = Vector2.one;
mainRect.offsetMin = Vector2.zero;
mainRect.offsetMax = Vector2.zero;
var mainBg = UIFactory.CreateUIObject("MainBackground", consoleBase);
var mainBgRect = mainBg.GetComponent<RectTransform>();
mainBgRect.pivot = new Vector2(0, 1);
mainBgRect.anchorMin = Vector2.zero;
mainBgRect.anchorMax = Vector2.one;
mainBgRect.offsetMin = Vector2.zero;
mainBgRect.offsetMax = Vector2.zero;
var mainBgImage = mainBg.AddComponent<Image>();
var lineHighlight = UIFactory.CreateUIObject("LineHighlight", consoleBase);
var lineHighlightRect = lineHighlight.GetComponent<RectTransform>();
lineHighlightRect.pivot = new Vector2(0.5f, 1);
lineHighlightRect.anchorMin = new Vector2(0, 1);
lineHighlightRect.anchorMax = new Vector2(1, 1);
lineHighlightRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 21);
var lineHighlightImage = lineHighlight.GetComponent<Image>();
if (!lineHighlightImage)
lineHighlightImage = lineHighlight.AddComponent<Image>();
var linesBg = UIFactory.CreateUIObject("LinesBackground", consoleBase);
var linesBgRect = linesBg.GetComponent<RectTransform>();
linesBgRect.anchorMin = Vector2.zero;
linesBgRect.anchorMax = new Vector2(0, 1);
linesBgRect.offsetMin = new Vector2(-17.5f, 0);
linesBgRect.offsetMax = new Vector2(17.5f, 0);
linesBgRect.sizeDelta = new Vector2(65, 0);
var linesBgImage = linesBg.AddComponent<Image>();
var inputObj = UIFactory.CreateTMPInput(consoleBase);
var inputField = inputObj.GetComponent<TMP_InputField>();
inputField.richText = false;
var inputRect = inputObj.GetComponent<RectTransform>();
inputRect.pivot = new Vector2(0.5f, 0.5f);
inputRect.anchorMin = Vector2.zero;
inputRect.anchorMax = new Vector2(0.92f, 1);
inputRect.offsetMin = new Vector2(20, 0);
inputRect.offsetMax = new Vector2(14, 0);
inputRect.anchoredPosition = new Vector2(40, 0);
var textAreaObj = inputObj.transform.Find("TextArea");
var textAreaRect = textAreaObj.GetComponent<RectTransform>();
textAreaRect.pivot = new Vector2(0.5f, 0.5f);
textAreaRect.anchorMin = Vector2.zero;
textAreaRect.anchorMax = Vector2.one;
var mainTextObj = textAreaObj.transform.Find("Text");
var mainTextRect = mainTextObj.GetComponent<RectTransform>();
mainTextRect.pivot = new Vector2(0.5f, 0.5f);
mainTextRect.anchorMin = Vector2.zero;
mainTextRect.anchorMax = Vector2.one;
mainTextRect.offsetMin = Vector2.zero;
mainTextRect.offsetMax = Vector2.zero;
var mainTextInput = mainTextObj.GetComponent<TextMeshProUGUI>();
mainTextInput.fontSize = 18;
var linesTextObj = UIFactory.CreateUIObject("LinesText", mainTextObj.gameObject);
var linesTextRect = linesTextObj.GetComponent<RectTransform>();
var linesTextInput = linesTextObj.AddComponent<TextMeshProUGUI>();
linesTextInput.fontSize = 18;
var highlightTextObj = UIFactory.CreateUIObject("HighlightText", mainTextObj.gameObject);
var highlightTextRect = highlightTextObj.GetComponent<RectTransform>();
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = Vector2.zero;
highlightTextRect.offsetMax = Vector2.zero;
var highlightTextInput = highlightTextObj.AddComponent<TextMeshProUGUI>();
highlightTextInput.fontSize = 18;
var scroll = UIFactory.CreateScrollbar(consoleBase);
var scrollRect = scroll.GetComponent<RectTransform>();
scrollRect.anchorMin = new Vector2(1, 0);
scrollRect.anchorMax = new Vector2(1, 1);
scrollRect.pivot = new Vector2(0.5f, 1);
scrollRect.offsetMin = new Vector2(-25f, 0);
var scroller = scroll.GetComponent<Scrollbar>();
scroller.direction = Scrollbar.Direction.TopToBottom;
var scrollColors = scroller.colors;
scrollColors.normalColor = new Color(0.6f, 0.6f, 0.6f, 1.0f);
scroller.colors = scrollColors;
var scrollImage = scroll.GetComponent<Image>();
var tmpInput = inputObj.GetComponent<TMP_InputField>();
tmpInput.scrollSensitivity = 15;
tmpInput.verticalScrollbar = scroller;
// set lines text anchors here after UI is fleshed out
linesTextRect.pivot = Vector2.zero;
linesTextRect.anchorMin = new Vector2(0, 0);
linesTextRect.anchorMax = new Vector2(1, 1);
linesTextRect.offsetMin = Vector2.zero;
linesTextRect.offsetMax = Vector2.zero;
linesTextRect.anchoredPosition = new Vector2(-40, 0);
tmpInput.GetComponentInChildren<RectMask2D>().enabled = false;
inputObj.GetComponent<Image>().enabled = false;
// setup button callbacks
compileButton.onClick.AddListener(new Action(() =>
{
if (!string.IsNullOrEmpty(tmpInput.text))
{
Evaluate(tmpInput.text.Trim());
}
}));
TMP_FontAsset fontToUse = null;
#if CPP
var fonts = ResourcesUnstrip.FindObjectsOfTypeAll(Il2CppType.Of<TMP_FontAsset>());
foreach (var font in fonts)
{
var fontCast = font.Il2CppCast(typeof(TMP_FontAsset)) as TMP_FontAsset;
if (fontCast.name.Contains("LiberationSans"))
{
fontToUse = fontCast;
break;
}
}
#else
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
foreach (var font in fonts)
{
if (font.name.Contains("LiberationSans"))
{
fontToUse = font;
break;
}
}
#endif
if (fontToUse != null)
{
fontToUse.faceInfo.tabWidth = 10;
fontToUse.tabSize = 10;
var faceInfo = fontToUse.faceInfo;
faceInfo.tabWidth = 10;
fontToUse.faceInfo = faceInfo;
tmpInput.fontAsset = fontToUse;
mainTextInput.font = fontToUse;
highlightTextInput.font = fontToUse;
}
try
{
codeEditor = new CodeEditor(inputField, mainTextInput, highlightTextInput, linesTextInput,
mainBgImage, lineHighlightImage, linesBgImage, scrollImage);
}
catch (Exception e)
{
ExplorerCore.Log(e);
}
}
#endregion
private class VoidType
{
public static readonly VoidType Value = new VoidType();
private VoidType() { }
}
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace ExplorerBeta.UI
@ -12,10 +13,10 @@ namespace ExplorerBeta.UI
{
private static Vector2 s_ThickElementSize = new Vector2(160f, 30f);
private static Vector2 s_ThinElementSize = new Vector2(160f, 20f);
//private static Vector2 s_ImageElementSize = new Vector2(100f, 100f);
private static Color s_DefaultSelectableColor = new Color(1f, 1f, 1f, 1f);
private static Color s_PanelColor = new Color(0.1f, 0.1f, 0.1f, 1.0f);
private static Color s_TextColor = new Color(0.95f, 0.95f, 0.95f, 1f);
//private static Color s_PanelColor = new Color(0.1f, 0.1f, 0.1f, 1.0f);
//private static Vector2 s_ImageElementSize = new Vector2(100f, 100f);
public static Resources UIResources { get; set; }
@ -30,7 +31,7 @@ namespace ExplorerBeta.UI
public Sprite mask;
}
private static GameObject CreateUIObject(string name, GameObject parent, Vector2 size = default)
public static GameObject CreateUIObject(string name, GameObject parent, Vector2 size = default)
{
GameObject obj = new GameObject(name);
@ -53,14 +54,22 @@ namespace ExplorerBeta.UI
//lbl.resizeTextForBestFit = true;
}
private static void SetDefaultColorTransitionValues(Selectable slider)
private static void SetDefaultColorTransitionValues(Selectable selectable)
{
ColorBlock colors = slider.colors;
colors.normalColor = new Color(0.3f, 0.3f, 0.3f);
ColorBlock colors = selectable.colors;
colors.normalColor = new Color(0.4f, 0.4f, 0.4f);
colors.highlightedColor = new Color(0.45f, 0.45f, 0.45f);
colors.pressedColor = new Color(0.1f, 0.1f, 0.1f);
colors.disabledColor = new Color(0.7f, 0.7f, 0.7f);
slider.colors = colors;
// fix to make all buttons become de-selected after being clicked.
// this is because i'm not setting any ColorBlock.selectedColor, because it is commonly stripped.
if (selectable is Button button)
{
button.onClick.AddListener(new Action(() => { button.OnDeselect(EventSystem.current?.baseEventDataCache); }));
}
selectable.colors = colors;
}
private static void SetParentAndAlign(GameObject child, GameObject parent)
@ -379,7 +388,7 @@ namespace ExplorerBeta.UI
var placeHolderObj = CreateUIObject("Placeholder", textArea);
var placeholderText = placeHolderObj.AddComponent<TextMeshProUGUI>();
placeholderText.fontSize = 16;
placeholderText.text = "Nothing logged yet...";
placeholderText.text = "...";
placeholderText.color = new Color(0.5f, 0.5f, 0.5f, 1.0f);
var placeHolderRect = placeHolderObj.GetComponent<RectTransform>();

View File

@ -71,6 +71,8 @@ namespace ExplorerBeta.UI
public static void Update()
{
MainMenu.Instance?.Update();
if (EventSys && InputModule)
{
if (EventSystem.current != EventSys)