diff --git a/src/Explorer.csproj b/src/Explorer.csproj index 173ba96..1f9a6e3 100644 --- a/src/Explorer.csproj +++ b/src/Explorer.csproj @@ -256,18 +256,16 @@ - - - - + + - - - + + + - - + + diff --git a/src/UI/Main/Pages/Console/Editor/AutoIndent.cs b/src/UI/Main/Pages/Console/Editor/AutoIndent.cs deleted file mode 100644 index dd66ddb..0000000 --- a/src/UI/Main/Pages/Console/Editor/AutoIndent.cs +++ /dev/null @@ -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; - - /// - /// Should auto indent be used for this language. - /// - public static bool allowAutoIndent = true; - /// - /// The character that causes the indent level to increase. - /// - public static char indentIncreaseCharacter = '{'; - /// - /// The character that causes the indent level to decrease. - /// - public static char indentDecreaseCharacter = '}'; - - // Properties - /// - /// Get the string representation of the indent character. - /// - 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"); - } - } -} diff --git a/src/UI/Main/Pages/Console/Editor/CSharpLexer.cs b/src/UI/Main/Pages/Console/Editor/CSharpLexer.cs new file mode 100644 index 0000000..ebda1d8 --- /dev/null +++ b/src/UI/Main/Pages/Console/Editor/CSharpLexer.cs @@ -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 matcherList = new List + { + 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; + } + } +} diff --git a/src/UI/Main/Pages/Console/Editor/CodeEditor.cs b/src/UI/Main/Pages/Console/Editor/CodeEditor.cs index 828cf7e..2b54379 100644 --- a/src/UI/Main/Pages/Console/Editor/CodeEditor.cs +++ b/src/UI/Main/Pages/Console/Editor/CodeEditor.cs @@ -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(); - 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(); - this.lineHighlightTransform = lineHighlight.GetComponent(); - - ApplyTheme(); - ApplyLanguage(); - - // subscribe to text input changing - InputField.onValueChanged.AddListener(new Action((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(); + this.lineHighlightTransform = lineHighlight.GetComponent(); - // 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 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(); + 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() diff --git a/src/UI/Main/Pages/Console/Editor/CodeTheme.cs b/src/UI/Main/Pages/Console/Editor/CodeTheme.cs deleted file mode 100644 index a2875f0..0000000 --- a/src/UI/Main/Pages/Console/Editor/CodeTheme.cs +++ /dev/null @@ -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" - } - }; - - /// - /// A symbol group used to specify which symbols should be highlighted. - /// - public static SymbolGroupMatch symbolGroup = new SymbolGroupMatch - { - symbols = @"[ ] ( ) . ? : + - * / % & | ^ ~ = < > ++ -- && || << >> == != <= >= - += -= *= /= %= &= |= ^= <<= >>= -> ?? =>", - highlightColor = new Color(0.58f, 0.47f, 0.37f, 1.0f), - }; - - /// - /// A number group used to specify whether numbers should be highlighted. - /// - public static NumberGroupMatch numberGroup = new NumberGroupMatch - { - highlightNumbers = true, - highlightColor = new Color(0.58f, 0.33f, 0.33f, 1.0f) - }; - - /// - /// A comment group used to specify which strings open and close comments. - /// - public static CommentGroupMatch commentGroup = new CommentGroupMatch - { - blockCommentEnd = @"*/", - blockCommentStart = @"/*", - lineCommentStart = @"//", - lineCommentHasPresedence = true, - highlightColor = new Color(0.34f, 0.65f, 0.29f, 1.0f), - }; - - /// - /// A literal group used to specify whether quote strings should be highlighted. - /// - public static LiteralGroupMatch literalGroup = new LiteralGroupMatch - { - highlightLiterals = true, - highlightColor = new Color(0.79f, 0.52f, 0.32f, 1.0f) - }; - - ///// - ///// Options group for all auto indent related settings. - ///// - //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 matcherList = new List - { - 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(); - } - } -} diff --git a/src/UI/Main/Pages/Console/Editor/InputTheme.cs b/src/UI/Main/Pages/Console/Editor/InputTheme.cs deleted file mode 100644 index 9299579..0000000 --- a/src/UI/Main/Pages/Console/Editor/InputTheme.cs +++ /dev/null @@ -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); - } -} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/CommentGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/CommentGroupMatch.cs deleted file mode 100644 index 8ba8b40..0000000 --- a/src/UI/Main/Pages/Console/Editor/Lexer/CommentGroupMatch.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Generic; -using Explorer.Unstrip.ColorUtility; -using UnityEngine; - -namespace Explorer.UI.Main.Pages.Console.Lexer -{ - /// - /// Used to match line and block comments. - /// - public sealed class CommentGroupMatch : MatchLexer - { - [NonSerialized] - private string htmlColor = null; - - // Public - /// - /// The string that denotes the start of a line comment. - /// Leave this value empty if line comments should not be highlighted. - /// - public string lineCommentStart; - /// - /// The string that denotes the start of a block comment. - /// Leave this value empty if block comments should not be highlighted. - /// - public string blockCommentStart; - /// - /// The string that denotes the end of a block comment. - /// - public string blockCommentEnd; - /// - /// The color that comments will be highlighted with. - /// - public Color highlightColor = Color.black; - - public bool lineCommentHasPresedence = true; - - // Properties - /// - /// 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. - /// - public bool HasCommentHighlighting - { - get - { - return string.IsNullOrEmpty(lineCommentStart) == false || - string.IsNullOrEmpty(blockCommentStart) == false; - } - } - - /// - /// Get the html tag color that comments will be highlighted with. - /// - public override string HTMLColor - { - get - { - // Build html color string - if (htmlColor == null) - htmlColor = "<#" + highlightColor.ToHex() + ">"; - - return htmlColor; - } - } - - /// - /// Returns an enumerable collection of characters from this group that can act as delimiter symbols when they appear after a keyword. - /// - public override IEnumerable SpecialStartCharacters - { - get - { - if (string.IsNullOrEmpty(lineCommentStart) == false) - yield return lineCommentStart[0]; - - if (string.IsNullOrEmpty(blockCommentEnd) == false) - yield return blockCommentEnd[0]; - } - } - - /// - /// Returns an enumerable collection of characters from this group that can act as delimiter symbols when they appear before a keyword. - /// - public override IEnumerable SpecialEndCharacters - { - get - { - if (string.IsNullOrEmpty(blockCommentEnd) == false) - yield return blockCommentEnd[blockCommentEnd.Length - 1]; - } - } - - // Methods - /// - /// Causes the cached values to be reloaded. - /// Useful for editor visualisation. - /// - public override void Invalidate() - { - this.htmlColor = null; - } - - /// - /// Returns true if the lexer input contains a valid comment format as the next character sequence. - /// - /// The input lexer - /// True if a comment was found or false if not - 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; - } - } -} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/CommentMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/CommentMatch.cs new file mode 100644 index 0000000..82533be --- /dev/null +++ b/src/UI/Main/Pages/Console/Editor/Lexer/CommentMatch.cs @@ -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 StartChars => new char[] { lineCommentStart[0], blockCommentStart[0] }; + public override IEnumerable 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'; + } +} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs b/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs index 112528f..7a506d0 100644 --- a/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs +++ b/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs @@ -6,62 +6,20 @@ using System.Threading.Tasks; namespace Explorer.UI.Main.Pages.Console.Lexer { - // Types - /// - /// Represents a keyword position where a special character may appear. - /// public enum SpecialCharacterPosition { - /// - /// The special character may appear before a keyword. - /// Start, - /// - /// The special character may appear after a keyword. - /// End, }; - /// - /// Represents a streamable lexer input which can be examined by matchers. - /// public interface ILexer { - // Properties - /// - /// Returns true if there are no more characters to read. - /// bool EndOfStream { get; } - - /// - /// Returns the previously read character or '\0' if there is no previous character. - /// char Previous { get; } - // Methods - /// - /// Attempt to read the next character. - /// - /// The next character in the stream or '\0' if the end of stream is reached char ReadNext(); - - /// - /// Causes the lexer to return its state to a previously commited state. - /// - /// Use -1 to return to the last commited state or a positive number to represent the number of characters to rollback void Rollback(int amount = -1); - - /// - /// Causes all read characters to be commited meaning that rollback will return to this lexer state. - /// void Commit(); - - /// - /// Determines whether the specified character is considered a special symbol by the lexer meaning that it is able to act as a delimiter. - /// - /// The character to check - /// The character position to check. This determines whether the character may appear at the start or end of a keyword - /// True if the character is a valid delimiter or false if not bool IsSpecialSymbol(char character, SpecialCharacterPosition position = SpecialCharacterPosition.Start); } } diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/InputStringLexer.cs b/src/UI/Main/Pages/Console/Editor/Lexer/InputLexer.cs similarity index 74% rename from src/UI/Main/Pages/Console/Editor/Lexer/InputStringLexer.cs rename to src/UI/Main/Pages/Console/Editor/Lexer/InputLexer.cs index 6afc22c..6c88b5b 100644 --- a/src/UI/Main/Pages/Console/Editor/Lexer/InputStringLexer.cs +++ b/src/UI/Main/Pages/Console/Editor/Lexer/InputLexer.cs @@ -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 specialStartSymbols = new HashSet(); @@ -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 LexInputString(string input) + public IEnumerable 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(); } } diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/KeywordGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/KeywordGroupMatch.cs deleted file mode 100644 index 1f8254c..0000000 --- a/src/UI/Main/Pages/Console/Editor/Lexer/KeywordGroupMatch.cs +++ /dev/null @@ -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 -{ - /// - /// A matcher that checks for a number of predefined keywords in the lexer stream. - /// - public sealed class KeywordGroupMatch : MatchLexer - { - // Private - private static readonly HashSet shortlist = new HashSet(); - private static readonly Stack removeList = new Stack(); - private string[] keywordCache = null; - private string htmlColor = null; - - // Public - /// - /// Used for editor gui only. Has no purpose other than to give the inspector foldout a nice name - /// - public string group = "Keyword Group"; // This value is not used - it just gives the inspector foldout a nice name - /// - /// A string containing one or more keywords separated by a space character that will be used by this matcher. - /// - public string keywords; - /// - /// The color that any matched keywords will be highlighted. - /// - public Color highlightColor = Color.black; - /// - /// Should keyword matching be case sensitive. - /// - public bool caseSensitive = true; - - // Properties - /// - /// Get a value indicating whether keyword highlighting is enabled based upon the number of valid keywords found. - /// - public bool HasKeywordHighlighting - { - get - { - // Check for valid keyword - if (string.IsNullOrEmpty(keywords) == false) - return true; - - return false; - } - } - - /// - /// Get the html formatted color tag that any matched keywords will be highlighted with. - /// - public override string HTMLColor - { - get - { - // Get html color - if (htmlColor == null) - htmlColor = "<#" + highlightColor.ToHex() + ">"; - - return htmlColor; - } - } - - // Methods - /// - /// Causes any cached data to be reloaded. - /// - public override void Invalidate() - { - this.htmlColor = null; - } - - /// - /// Check whether the specified lexer has a valid keyword at its current position. - /// - /// The input lexer to check - /// True if the stream has a keyword or false if not - 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(); - 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; - } - } -} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/KeywordMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/KeywordMatch.cs new file mode 100644 index 0000000..893b6fa --- /dev/null +++ b/src/UI/Main/Pages/Console/Editor/Lexer/KeywordMatch.cs @@ -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 shortlist = new HashSet(); + private readonly Stack removeList = new Stack(); + 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(); + 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)); + } +} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/LiteralGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/LiteralGroupMatch.cs deleted file mode 100644 index 4601148..0000000 --- a/src/UI/Main/Pages/Console/Editor/Lexer/LiteralGroupMatch.cs +++ /dev/null @@ -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 -{ - /// - /// A matcher that checks for quote strings in the lexer stream. - /// - [Serializable] - public sealed class LiteralGroupMatch : MatchLexer - { - // Private - private string htmlColor = null; - - // Public - /// - /// Should literal be highlighted. - /// When true, any text surrounded by double quotes will be highlighted. - /// - public bool highlightLiterals = true; - /// - /// The color that any matched literals will be highlighted. - /// - public Color highlightColor = Color.black; - - // Properties - /// - /// Get a value indicating whether literal highlighting is enabled. - /// - public bool HasLiteralHighlighting - { - get { return highlightLiterals; } - } - - /// - /// Get the html formatted color tag that any matched literals will be highlighted with. - /// - public override string HTMLColor - { - get - { - if (htmlColor == null) - htmlColor = "<#" + highlightColor.ToHex() + ">"; - - return htmlColor; - } - } - - /// - /// Returns special symbols that can act as delimiters when appearing before a word. - /// In this case '"' will be returned. - /// - public override IEnumerable SpecialStartCharacters - { - get - { - yield return '"'; - } - } - - /// - /// Returns special symbols that can act as delimiters when appearing after a word. - /// In this case '"' will be returned. - /// - public override IEnumerable SpecialEndCharacters - { - get - { - yield return '"'; - } - } - - // Methods - /// - /// Causes any cached data to be reloaded. - /// - public override void Invalidate() - { - this.htmlColor = null; - } - - /// - /// Check whether the specified lexer has a valid literal at its current position. - /// - /// The input lexer to check - /// True if the stream has a literal or false if not - 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; - } - } -} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs b/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs index 97560c2..09d6ede 100644 --- a/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs +++ b/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs @@ -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 { - /// - /// Get the html formatted color tag that any matched text will be highlighted with. - /// - public abstract string HTMLColor { get; } + public abstract Color HighlightColor { get; } - /// - /// Get an enumerable collection of special characters that can act as delimiter symbols when they appear before a word. - /// - public virtual IEnumerable SpecialStartCharacters { get { yield break; } } + public string HexColor => htmlColor ?? (htmlColor = "<#" + HighlightColor.ToHex() + ">"); + private string htmlColor = null; - /// - /// Get an enumerable collection of special characters that can act as delimiter symbols when they appear after a word. - /// - public virtual IEnumerable SpecialEndCharacters { get { yield break; } } + public virtual IEnumerable StartChars { get { yield break; } } + public virtual IEnumerable EndChars { get { yield break; } } - // Methods - /// - /// Checks the specified lexers current position for a certain sequence of characters as defined by the inheriting matcher. - /// - /// - /// public abstract bool IsImplicitMatch(ILexer lexer); - /// - /// Causes the matcher to invalidate any cached data forcing it to be regenerated or reloaded. - /// - public virtual void Invalidate() { } - - /// - /// Attempts to check for a match in the specified lexer. - /// - /// The lexer that will be checked - /// True if a match was found or false if not 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; } } } diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/NumberGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/NumberGroupMatch.cs deleted file mode 100644 index 2effc0a..0000000 --- a/src/UI/Main/Pages/Console/Editor/Lexer/NumberGroupMatch.cs +++ /dev/null @@ -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 -{ - /// - /// A matcher that checks for any numbers that appear in the lexer stream. - /// - public sealed class NumberGroupMatch : MatchLexer - { - // Private - private string htmlColor = null; - - // Public - /// - /// Should number highlighting be used. - /// When false, numbers will appear in the default text color as defined by the current editor theme. - /// - public bool highlightNumbers = true; - /// - /// The color that any matched numbers will be highlighted. - /// - public Color highlightColor = Color.black; - - // Properties - /// - /// Get a value indicating whether keyword highlighting is enabled. - /// - public bool HasNumberHighlighting - { - get { return highlightNumbers; } - } - - /// - /// Get the html formatted color tag that any matched numbers will be highlighted with. - /// - public override string HTMLColor - { - get - { - if (htmlColor == null) - htmlColor = "<#" + highlightColor.ToHex() + ">"; - - return htmlColor; - } - } - - // Methods - /// - /// Causes any cached data to be reloaded. - /// - public override void Invalidate() - { - this.htmlColor = null; - } - - /// - /// Check whether the specified lexer has a valid number sequence at its current position. - /// - /// The input lexer to check - /// True if the stream has a number sequence or false if not - 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 == '.'; - } - -} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/NumberMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/NumberMatch.cs new file mode 100644 index 0000000..0af3437 --- /dev/null +++ b/src/UI/Main/Pages/Console/Editor/Lexer/NumberMatch.cs @@ -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 == '.'; + } + +} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/StringMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/StringMatch.cs new file mode 100644 index 0000000..3f528b0 --- /dev/null +++ b/src/UI/Main/Pages/Console/Editor/Lexer/StringMatch.cs @@ -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 StartChars { get { yield return '"'; } } + public override IEnumerable 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; + } + } +} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/SymbolGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/SymbolGroupMatch.cs deleted file mode 100644 index 53dbced..0000000 --- a/src/UI/Main/Pages/Console/Editor/Lexer/SymbolGroupMatch.cs +++ /dev/null @@ -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 -{ - /// - /// A matcher that checks for a number of predefined symbols in the lexer stream. - /// - [Serializable] - public sealed class SymbolGroupMatch : MatchLexer - { - // Private - private static readonly List shortlist = new List(); - private static readonly Stack removeList = new Stack(); - [NonSerialized] - private string[] symbolCache = null; - [NonSerialized] - private string htmlColor = null; - - // Public - /// - /// 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. - /// - public string symbols; - /// - /// The color that any matched symbols will be highlighted. - /// - public Color highlightColor = Color.black; - - // Properties - /// - /// Get a value indicating whether symbol highlighting is enabled based upon the number of valid symbols found. - /// - public bool HasSymbolHighlighting - { - get { return symbols.Length > 0; } - } - - /// - /// Get the html formatted color tag that any matched symbols will be highlighted with. - /// - public override string HTMLColor - { - get - { - // Get html color - if (htmlColor == null) - htmlColor = "<#" + highlightColor.ToHex() + ">"; - - return htmlColor; - } - } - - /// - /// Returns special symbols that can act as delimiters when appearing before a word. - /// - public override IEnumerable 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]; - } - } - - /// - /// Returns special symbols that can act as delimiters when appearing after a word. - /// In this case '"' will be returned. - /// - public override IEnumerable 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 - /// - /// Causes any cached data to be reloaded. - /// - public override void Invalidate() - { - this.htmlColor = null; - } - - /// - /// Checks whether the specified lexer has a valid symbol at its current posiiton. - /// - /// The input lexer to check - /// True if the stream has a symbol or false if not - 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(); - foreach (var sym in symSplit) - { - if (!string.IsNullOrEmpty(sym) && sym.Length > 0) - { - list.Add(sym); - } - } - symbolCache = list.ToArray(); - } - } - } -} diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/SymbolMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/SymbolMatch.cs new file mode 100644 index 0000000..9ff07ff --- /dev/null +++ b/src/UI/Main/Pages/Console/Editor/Lexer/SymbolMatch.cs @@ -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 shortlist = new List(); + private static readonly Stack removeList = new Stack(); + private string[] symbolCache = null; + + public override IEnumerable StartChars + { + get + { + BuildSymbolCache(); + foreach (string symbol in symbolCache.Where(x => x.Length > 0)) + yield return symbol[0]; + } + } + + public override IEnumerable 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(); + foreach (var sym in symSplit) + { + if (!string.IsNullOrEmpty(sym) && sym.Length > 0) + { + list.Add(sym); + } + } + symbolCache = list.ToArray(); + } + } +} diff --git a/src/UI/Main/Pages/ConsolePage.cs b/src/UI/Main/Pages/ConsolePage.cs index bd0b42e..1ea296d 100644 --- a/src/UI/Main/Pages/ConsolePage.cs +++ b/src/UI/Main/Pages/ConsolePage.cs @@ -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(); } + 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(); - topBarLayout.preferredHeight = 50; + topBarLayout.minHeight = 50; topBarLayout.flexibleHeight = 0; var topBarGroup = topBarObj.GetComponent(); @@ -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)