cleanup and refactor code editor

This commit is contained in:
sinaioutlander 2020-10-26 01:07:59 +11:00
parent 32684bc63e
commit 2256828384
20 changed files with 788 additions and 1565 deletions

View File

@ -256,18 +256,16 @@
<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\CSharpLexer.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\CommentMatch.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\InputLexer.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\KeywordMatch.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\StringMatch.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\Editor\Lexer\NumberMatch.cs" />
<Compile Include="UI\Main\Pages\Console\Editor\Lexer\SymbolMatch.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" />

View File

@ -1,133 +0,0 @@
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,173 @@
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 CSharpLexer
{
// todo option to disable auto-indent
public static bool allowAutoIndent = true;
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 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
ref remove return sbyte select short sizeof stackalloc string
struct switch this throw true try typeof uint ulong unchecked unsafe ushort
value var where while yield"
};
public static KeywordMatch invalidKeywordMatcher = new KeywordMatch()
{
highlightColor = new Color(0.95f, 0.10f, 0.10f, 1.0f),
keywords = @"abstract async class delegate explicit extern get
implicit interface internal namespace operator override private protected public
using partial readonly sealed set static 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 MatchLexer[] matchers = null;
internal static MatchLexer[] Matchers
{
get
{
if (matchers == null)
{
List<MatchLexer> matcherList = new List<MatchLexer>
{
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;
}
}
}

View File

@ -7,60 +7,16 @@ using System.Reflection;
using ExplorerBeta.Input;
using Explorer.UI.Main.Pages.Console.Lexer;
using ExplorerBeta;
using System.Linq;
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;
{
private readonly InputLexer inputLexer = new InputLexer();
var highlightTextRect = inputHighlightText.GetComponent<RectTransform>();
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = Vector2.zero;
highlightTextRect.offsetMax = Vector2.zero;
public TMP_InputField InputField { get; }
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;
@ -69,13 +25,26 @@ namespace Explorer.UI.Main.Pages.Console
private readonly Image lineNumberBackground;
private readonly Image scrollbar;
private bool lineNumbers = true;
private int lineNumbersSize = 20;
private string lastText;
private bool lineHighlightLocked;
private readonly RectTransform inputTextTransform;
private readonly RectTransform lineHighlightTransform;
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 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
{
@ -97,64 +66,39 @@ namespace Explorer.UI.Main.Pages.Console
}
}
public string HighlightedText => inputHighlightText.text;
public bool LineNumbers
public CodeEditor(TMP_InputField inputField, TextMeshProUGUI inputText, TextMeshProUGUI inputHighlightText, TextMeshProUGUI lineText,
Image background, Image lineHighlight, Image lineNumberBackground, Image scrollbar)
{
get { return lineNumbers; }
set
this.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())
{
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);
//}
throw new Exception("References are missing!");
}
}
// todo maybe not needed
public int LineNumbersSize
{
get { return lineNumbersSize; }
set
{
lineNumbersSize = value;
this.inputTextTransform = inputText.GetComponent<RectTransform>();
this.lineHighlightTransform = lineHighlight.GetComponent<RectTransform>();
// Update the line numbers
LineNumbers = lineNumbers;
}
ApplyTheme();
inputLexer.UseMatchers(CSharpLexer.DelimiterSymbols, CSharpLexer.Matchers);
// subscribe to text input changing
this.InputField.onValueChanged.AddListener(new Action<string>((string s) => { OnInputChanged(); }));
}
public void Update()
{
// Auto indent
if (AutoIndent.autoIndentMode != AutoIndent.IndentMode.None)
// Check for new line
if (InputManager.GetKeyDown(KeyCode.Return))
{
// Check for new line
if (InputManager.GetKeyDown(KeyCode.Return))
{
AutoIndentCaret();
}
AutoIndentCaret();
}
bool focusKeyPressed = false;
@ -172,54 +116,16 @@ namespace Explorer.UI.Main.Pages.Console
// Update line highlight
if (focusKeyPressed || InputManager.GetMouseButton(0))
{
UpdateCurrentLineHighlight();
UpdateHighlight();
}
}
public void Refresh(bool forceUpdate = false)
public void OnInputChanged(bool forceUpdate = false)
{
// Trigger a content change event
DisplayedContentChanged(InputField.text, forceUpdate);
}
var newText = InputField.text;
public void SetLineHighlight(int lineNumber, bool lockLineHighlight)
{
// Check if code editor is not active
if (lineNumber < 1 || lineNumber > LineCount)
return;
UpdateIndent();
//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))
@ -227,37 +133,45 @@ namespace Explorer.UI.Main.Pages.Console
inputHighlightText.text = string.Empty;
}
// Its possible the text was cleared so we need to sync numbers and highlighter
UpdateCurrentLineNumbers();
UpdateCurrentLineHighlight();
UpdateLineNumbers();
UpdateHighlight();
return;
}
inputHighlightText.text = SyntaxHighlightContent(newText);
// Sync line numbers and update the line highlight
UpdateCurrentLineNumbers();
UpdateCurrentLineHighlight();
UpdateLineNumbers();
UpdateHighlight();
this.lastText = newText;
}
private void UpdateCurrentLineNumbers()
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()
{
// 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)
@ -292,7 +206,6 @@ namespace Explorer.UI.Main.Pages.Console
}
}
// Update displayed line numbers
lineText.text = lineBuilder.ToString();
LineCount = currentLineCount;
}
@ -300,9 +213,8 @@ namespace Explorer.UI.Main.Pages.Console
}
}
private void UpdateCurrentLineColumnIndent()
private void UpdateIndent()
{
// Get the current line number
int caret = InputField.caretPosition;
if (caret < 0 || caret >= inputText.textInfo.characterInfo.Count)
@ -318,48 +230,35 @@ namespace Explorer.UI.Main.Pages.Console
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++)
{
for (int i = 0; i < caret && i < InputField.text.Length; i++)
{
char character = InputField.text[i];
char character = InputField.text[i];
// Check for opening indents
if (character == AutoIndent.indentIncreaseCharacter)
CurrentIndent++;
if (character == CSharpLexer.indentIncreaseCharacter)
CurrentIndent++;
// Check for closing indents
if (character == AutoIndent.indentDecreaseCharacter)
CurrentIndent--;
}
// Dont allow negative indents
if (CurrentIndent < 0)
CurrentIndent = 0;
if (character == CSharpLexer.indentDecreaseCharacter)
CurrentIndent--;
}
if (CurrentIndent < 0)
CurrentIndent = 0;
}
private void UpdateCurrentLineHighlight()
private void UpdateHighlight()
{
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;
@ -378,44 +277,33 @@ namespace Explorer.UI.Main.Pages.Console
private string SyntaxHighlightContent(string inputText)
{
if (!InputTheme.allowSyntaxHighlighting)
return inputText;
int offset = 0;
highlightedBuilder.Length = 0;
foreach (var match in lexer.LexInputString(inputText))
foreach (var match in inputLexer.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)
@ -429,7 +317,7 @@ namespace Explorer.UI.Main.Pages.Console
var indentMinusOne = indent.Substring(0, indent.Length - 1);
// get last index of {
// check it on the next line if its not already
// chuck 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("{");
@ -444,37 +332,9 @@ namespace Explorer.UI.Main.Pages.Console
}
// 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;
}
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
@ -490,14 +350,14 @@ namespace Explorer.UI.Main.Pages.Console
}
// Update line column and indent positions
UpdateCurrentLineColumnIndent();
UpdateIndent();
inputText.text = InputField.text;
inputText.SetText(InputField.text, true);
inputText.Rebuild(CanvasUpdate.Prelayout);
InputField.ForceLabelUpdate();
InputField.Rebuild(CanvasUpdate.Prelayout);
Refresh(true);
OnInputChanged(true);
}
private string GetAutoIndentTab(int amount)
@ -510,26 +370,34 @@ namespace Explorer.UI.Main.Pages.Console
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()
{
// Check for missing references
if (!AllReferencesAssigned())
throw new Exception("Cannot apply theme because one or more required component references are missing. ");
var highlightTextRect = inputHighlightText.GetComponent<RectTransform>();
highlightTextRect.anchorMin = Vector2.zero;
highlightTextRect.anchorMax = Vector2.one;
highlightTextRect.offsetMin = Vector2.zero;
highlightTextRect.offsetMax = Vector2.zero;
// 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;
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 void ApplyLanguage()
{
lexer.UseMatchers(CodeTheme.DelimiterSymbols, CodeTheme.Matchers);
}
private bool AllReferencesAssigned()

View File

@ -1,164 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,221 +0,0 @@
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,47 @@
using System;
using System.Collections.Generic;
using Explorer.Unstrip.ColorUtility;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
public sealed class CommentMatch : MatchLexer
{
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(ILexer lexer) => IsMatch(lexer, lineCommentStart) || IsMatch(lexer, blockCommentStart);
private bool IsMatch(ILexer 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(ILexer lexer, char character) => lexer.EndOfStream || character == '\n' || character == '\r';
}
}

View File

@ -6,62 +6,20 @@ 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

@ -6,17 +6,15 @@ using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
internal struct InputStringMatchInfo
internal struct LexerMatchInfo
{
// Public
public int startIndex;
public int endIndex;
public string htmlColor;
}
internal class InputStringLexer : ILexer
internal class InputLexer : ILexer
{
// Private
private string inputString = null;
private MatchLexer[] matchers = null;
private readonly HashSet<char> specialStartSymbols = new HashSet<char>();
@ -26,7 +24,6 @@ namespace Explorer.UI.Main.Pages.Console.Lexer
private int currentIndex = 0;
private int currentLookaheadIndex = 0;
// Properties
public bool EndOfStream
{
get { return currentLookaheadIndex >= inputString.Length; }
@ -37,102 +34,82 @@ namespace Explorer.UI.Main.Pages.Console.Lexer
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)
foreach (char special in lexer.StartChars)
if (specialStartSymbols.Contains(special) == false)
specialStartSymbols.Add(special);
foreach (char special in lexer.SpecialEndCharacters)
foreach (char special in lexer.EndChars)
if (specialEndSymbols.Contains(special) == false)
specialEndSymbols.Add(special);
}
}
}
public IEnumerable<InputStringMatchInfo> LexInputString(string input)
public IEnumerable<LexerMatchInfo> 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
yield return new LexerMatchInfo
{
startIndex = startIndex,
endIndex = endIndex,
htmlColor = matcher.HTMLColor,
htmlColor = matcher.HexColor,
};
// Move to next character
break;
}
}
if (didMatchLexer == false)
{
// Move to next
ReadNext();
Commit();
}
@ -141,14 +118,11 @@ namespace Explorer.UI.Main.Pages.Console.Lexer
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++;
@ -159,7 +133,6 @@ namespace Explorer.UI.Main.Pages.Console.Lexer
{
if (amount == -1)
{
// Revert to index
currentLookaheadIndex = currentIndex;
}
else
@ -193,14 +166,11 @@ namespace Explorer.UI.Main.Pages.Console.Lexer
private void ReadWhiteSpace()
{
// Read until white space
while (char.IsWhiteSpace(ReadNext()) == true)
{
// Consume the char
Commit();
}
// Return lexer state
Rollback();
}
}

View File

@ -1,208 +0,0 @@
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,110 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Explorer.Unstrip.ColorUtility;
using ExplorerBeta;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
public sealed class KeywordMatch : MatchLexer
{
public string keywords;
public override Color HighlightColor => this.highlightColor;
public Color highlightColor;
private readonly HashSet<string> shortlist = new HashSet<string>();
private readonly Stack<string> removeList = new Stack<string>();
private string[] keywordCache = null;
public override bool IsImplicitMatch(ILexer 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 (CompareChar(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 ||
!CompareChar(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)
{
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) =>
(a == b) || (char.ToUpper(a, CultureInfo.CurrentCulture) == char.ToUpper(b, CultureInfo.CurrentCulture));
}
}

View File

@ -1,120 +0,0 @@
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

@ -1,61 +1,33 @@
using System;
using System.Collections.Generic;
using Explorer.Unstrip.ColorUtility;
using ExplorerBeta;
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; }
public abstract Color HighlightColor { 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; } }
public string HexColor => htmlColor ?? (htmlColor = "<#" + HighlightColor.ToHex() + ">");
private string htmlColor = null;
/// <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; } }
public virtual IEnumerable<char> StartChars { get { yield break; } }
public virtual IEnumerable<char> EndChars { 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)
if (IsImplicitMatch(lexer))
{
// Consume read tokens
lexer.Commit();
}
else
{
// Revert lexer state
lexer.Rollback();
return true;
}
return match;
lexer.Rollback();
return false;
}
}
}

View File

@ -1,105 +0,0 @@
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,44 @@
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
{
public sealed class NumberMatch : MatchLexer
{
public override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f);
public override bool IsImplicitMatch(ILexer 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Explorer.Unstrip.ColorUtility;
using UnityEngine;
namespace Explorer.UI.Main.Pages.Console.Lexer
{
public sealed class StringMatch : MatchLexer
{
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(ILexer lexer)
{
if (lexer.ReadNext() == '"')
{
while (!IsClosingQuoteOrEndFile(lexer, lexer.ReadNext())) ;
return true;
}
return false;
}
private bool IsClosingQuoteOrEndFile(ILexer lexer, char character)
{
if (lexer.EndOfStream == true ||
character == '"')
{
return true;
}
return false;
}
}
}

View File

@ -1,223 +0,0 @@
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,137 @@
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
{
public sealed class SymbolMatch : MatchLexer
{
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(ILexer 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;
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

@ -21,7 +21,7 @@ namespace Explorer.UI.Main.Pages
public static ConsolePage Instance { get; private set; }
private CodeEditor codeEditor;
private CodeEditor m_codeEditor;
private ScriptEvaluator m_evaluator;
@ -62,7 +62,7 @@ namespace Explorer.UI.Main.Pages
public override void Update()
{
codeEditor?.Update();
m_codeEditor?.Update();
}
internal string AsmToUsing(string asm, bool richText = false)
@ -121,6 +121,110 @@ namespace Explorer.UI.Main.Pages
UsingDirectives = new List<string>();
}
private static string m_prevInput = "NULL";
// TODO call from OnInputChanged
private void CheckAutocomplete()
{
var input = m_codeEditor.InputField.text;
var caretIndex = m_codeEditor.InputField.caretPosition;
if (!string.IsNullOrEmpty(input))
{
try
{
var splitChars = new[] { ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&' };
// Credit ManlyMarco
// Separate input into parts, grab only the part with cursor in it
var start = caretIndex <= 0 ? 0 : input.LastIndexOfAny(splitChars, (int)(caretIndex - 1)) + 1;
var end = caretIndex <= 0 ? input.Length : input.IndexOfAny(splitChars, (int)(caretIndex - 1));
if (end < 0 || end < start) end = input.Length;
input = input.Substring(start, end - start);
}
catch (ArgumentException) { }
if (!string.IsNullOrEmpty(input) && input != m_prevInput)
{
GetAutocompletes(input);
}
}
else
{
ClearAutocompletes();
}
m_prevInput = input;
}
private void ClearAutocompletes()
{
if (AutoCompletes.Any())
{
AutoCompletes.Clear();
}
}
private void UseAutocomplete(string suggestion)
{
int cursorIndex = m_codeEditor.InputField.caretPosition;
var input = m_codeEditor.InputField.text;
input = input.Insert(cursorIndex, suggestion);
m_codeEditor.InputField.text = input;
ClearAutocompletes();
}
private void GetAutocompletes(string input)
{
try
{
//ExplorerCore.Log("Fetching suggestions for input " + input);
// Credit ManylMarco
AutoCompletes.Clear();
var completions = m_evaluator.GetCompletions(input, out string prefix);
if (completions != null)
{
if (prefix == null)
prefix = input;
AutoCompletes.AddRange(completions
.Where(x => !string.IsNullOrEmpty(x))
.Select(x => new AutoComplete(x, prefix, AutoComplete.Contexts.Other))
);
}
var trimmed = input.Trim();
if (trimmed.StartsWith("using"))
trimmed = trimmed.Remove(0, 5).Trim();
var namespaces = AutoCompleteHelpers.Namespaces
.Where(x => x.StartsWith(trimmed) && x.Length > trimmed.Length)
.Select(x => new AutoComplete(
x.Substring(trimmed.Length),
x.Substring(0, trimmed.Length),
AutoComplete.Contexts.Namespace));
AutoCompletes.AddRange(namespaces);
}
catch (Exception ex)
{
ExplorerCore.Log("C# Console error:\r\n" + ex);
ClearAutocompletes();
}
}
// Call on OnInputChanged, or maybe limit frequency if its too laggy
// update autocomplete buttons
private void RefreshAutocompleteButtons()
{
throw new NotImplementedException("TODO");
}
#region UI Construction
public void ConstructUI()
@ -139,7 +243,7 @@ namespace Explorer.UI.Main.Pages
var topBarObj = UIFactory.CreateHorizontalGroup(Content);
var topBarLayout = topBarObj.AddComponent<LayoutElement>();
topBarLayout.preferredHeight = 50;
topBarLayout.minHeight = 50;
topBarLayout.flexibleHeight = 0;
var topBarGroup = topBarObj.GetComponent<HorizontalLayoutGroup>();
@ -348,7 +452,7 @@ namespace Explorer.UI.Main.Pages
try
{
codeEditor = new CodeEditor(inputField, mainTextInput, highlightTextInput, linesTextInput,
m_codeEditor = new CodeEditor(inputField, mainTextInput, highlightTextInput, linesTextInput,
mainBgImage, lineHighlightImage, linesBgImage, scrollImage);
}
catch (Exception e)