using System; using System.Linq; using System.Text; using UnityExplorer.Input; using UnityExplorer.Console.Lexer; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using UnityExplorer.UI; using UnityExplorer.UI.PageModel; using System.Collections.Generic; using System.Reflection; #if CPP using UnityExplorer.Unstrip.Resources; using UnityExplorer.Helpers; using UnhollowerRuntimeLib; #endif namespace UnityExplorer.Console { public class CodeEditor { private readonly InputLexer inputLexer = new InputLexer(); public TMP_InputField InputField { get; internal set; } public TextMeshProUGUI inputText; private TextMeshProUGUI inputHighlightText; private TextMeshProUGUI lineText; private Image background; private Image lineNumberBackground; private Image scrollbar; //private readonly RectTransform inputTextTransform; //private readonly RectTransform lineHighlightTransform; //private bool lineHighlightLocked; //private Image lineHighlight; public int LineCount { get; private set; } public int CurrentLine { get; private set; } public int CurrentColumn { get; private set; } public int CurrentIndent { get; private set; } private static readonly StringBuilder highlightedBuilder = new StringBuilder(4096); private static readonly StringBuilder lineBuilder = new StringBuilder(); private static readonly KeyCode[] lineChangeKeys = { KeyCode.Return, KeyCode.Backspace, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow }; public string HighlightedText => inputHighlightText.text; public string Text { get { return InputField.text; } set { if (!string.IsNullOrEmpty(value)) { InputField.text = value; inputHighlightText.text = value; } else { InputField.text = string.Empty; inputHighlightText.text = string.Empty; } inputText.ForceMeshUpdate(false); } } internal const string STARTUP_TEXT = @"Welcome to the UnityExplorer C# Console. The following helper methods are available: * Log(""message"") logs a message to the debug console * CurrentTarget() returns the currently inspected target on the Home page * AllTargets() returns an object[] array containing all inspected instances * Inspect(someObject) to inspect an instance, eg. Inspect(Camera.main); * Inspect(typeof(SomeClass)) to inspect a Class with static reflection * AddUsing(""SomeNamespace"") adds a using directive to the C# console * GetUsing() logs the current using directives to the debug console * Reset() resets all using directives and variables "; public CodeEditor() { ConstructUI(); if (!AllReferencesAssigned()) { throw new Exception("References are missing!"); } //inputTextTransform = inputText.GetComponent(); //lineHighlightTransform = lineHighlight.GetComponent(); ApplyTheme(); inputLexer.UseMatchers(CSharpLexer.DelimiterSymbols, CSharpLexer.Matchers); // subscribe to text input changing #if CPP InputField.onValueChanged.AddListener(new Action((string s) => { OnInputChanged(); })); #else this.InputField.onValueChanged.AddListener((string s) => { OnInputChanged(); }); #endif } public void Update() { // Check for new line if (ConsolePage.EnableAutoIndent && InputManager.GetKeyDown(KeyCode.Return)) { AutoIndentCaret(); } if (EventSystem.current?.currentSelectedGameObject?.name == "InputField (TMP)") { bool focusKeyPressed = false; // Check for any focus key pressed foreach (KeyCode key in lineChangeKeys) { if (InputManager.GetKeyDown(key)) { focusKeyPressed = true; break; } } // Update line highlight if (focusKeyPressed || InputManager.GetMouseButton(0)) { //UpdateHighlight(); ConsolePage.Instance.OnInputChanged(); } } } public void OnInputChanged(bool forceUpdate = false) { string newText = InputField.text; UpdateIndent(); if (!forceUpdate && string.IsNullOrEmpty(newText)) { inputHighlightText.text = string.Empty; } else { inputHighlightText.text = SyntaxHighlightContent(newText); } UpdateLineNumbers(); //UpdateHighlight(); ConsolePage.Instance.OnInputChanged(); } //public void SetLineHighlight(int lineNumber, bool lockLineHighlight) //{ // if (lineNumber < 1 || lineNumber > LineCount) // { // return; // } // lineHighlightTransform.anchoredPosition = new Vector2(5, // (inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight * // //-(lineNumber - 1)) - 4f + // -(lineNumber - 1)) + // inputTextTransform.anchoredPosition.y); // lineHighlightLocked = lockLineHighlight; //} private void UpdateLineNumbers() { int currentLineCount = inputText.textInfo.lineCount; int currentLineNumber = 1; if (currentLineCount != LineCount) { try { lineBuilder.Length = 0; for (int i = 1; i < currentLineCount + 2; i++) { if (i - 1 > 0 && i - 1 < currentLineCount - 1) { int characterStart = inputText.textInfo.lineInfo[i - 1].firstCharacterIndex; int characterCount = inputText.textInfo.lineInfo[i - 1].characterCount; if (characterStart >= 0 && characterStart < inputText.text.Length && characterCount != 0 && !inputText.text.Substring(characterStart, characterCount).Contains("\n")) { lineBuilder.Append("\n"); continue; } } lineBuilder.Append(currentLineNumber); lineBuilder.Append('\n'); currentLineNumber++; if (i - 1 == 0 && i - 1 < currentLineCount - 1) { int characterStart = inputText.textInfo.lineInfo[i - 1].firstCharacterIndex; int characterCount = inputText.textInfo.lineInfo[i - 1].characterCount; if (characterStart >= 0 && characterStart < inputText.text.Length && characterCount != 0 && !inputText.text.Substring(characterStart, characterCount).Contains("\n")) { lineBuilder.Append("\n"); continue; } } } lineText.text = lineBuilder.ToString(); LineCount = currentLineCount; } catch { } } } private void UpdateIndent() { int caret = InputField.caretPosition; if (caret < 0 || caret >= inputText.textInfo.characterInfo.Length) { while (caret >= 0 && caret >= inputText.textInfo.characterInfo.Length) { caret--; } if (caret < 0 || caret >= inputText.textInfo.characterInfo.Length) { return; } } CurrentLine = inputText.textInfo.characterInfo[caret].lineNumber; int charCount = 0; for (int i = 0; i < CurrentLine; i++) { charCount += inputText.textInfo.lineInfo[i].characterCount; } CurrentColumn = caret - charCount; CurrentIndent = 0; for (int i = 0; i < caret && i < InputField.text.Length; i++) { char character = InputField.text[i]; if (character == CSharpLexer.indentIncreaseCharacter) { CurrentIndent++; } if (character == CSharpLexer.indentDecreaseCharacter) { CurrentIndent--; } } if (CurrentIndent < 0) { CurrentIndent = 0; } } //private void UpdateHighlight() //{ // if (lineHighlightLocked) // { // return; // } // try // { // int caret = InputField.caretPosition - 1; // float lineHeight = inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight; // int lineNumber = inputText.textInfo.characterInfo[caret].lineNumber; // float offset = lineNumber + inputTextTransform.anchoredPosition.y; // lineHighlightTransform.anchoredPosition = new Vector2(5, -(offset * lineHeight)); // } // catch //(Exception e) // { // //ExplorerCore.LogWarning("Exception on Update Line Highlight: " + e); // } //} private const string CLOSE_COLOR_TAG = ""; private string SyntaxHighlightContent(string inputText) { int offset = 0; highlightedBuilder.Length = 0; foreach (LexerMatchInfo match in inputLexer.LexInputString(inputText)) { for (int i = offset; i < match.startIndex; i++) { highlightedBuilder.Append(inputText[i]); } highlightedBuilder.Append(match.htmlColor); for (int i = match.startIndex; i < match.endIndex; i++) { highlightedBuilder.Append(inputText[i]); } highlightedBuilder.Append(CLOSE_COLOR_TAG); offset = match.endIndex; } for (int i = offset; i < inputText.Length; i++) { highlightedBuilder.Append(inputText[i]); } inputText = highlightedBuilder.ToString(); return inputText; } private void AutoIndentCaret() { if (CurrentIndent > 0) { string indent = GetAutoIndentTab(CurrentIndent); if (indent.Length > 0) { int caretPos = InputField.caretPosition; string indentMinusOne = indent.Substring(0, indent.Length - 1); // get last index of { // chuck it on the next line if its not already string text = InputField.text; string sub = InputField.text.Substring(0, InputField.caretPosition); int lastIndex = sub.LastIndexOf("{"); int offset = lastIndex - 1; if (offset >= 0 && text[offset] != '\n' && text[offset] != '\t') { string open = "\n" + indentMinusOne; InputField.text = text.Insert(offset + 1, open); caretPos += open.Length; } // check if should add auto-close } int numOpen = InputField.text.Where(x => x == CSharpLexer.indentIncreaseCharacter).Count(); int numClose = InputField.text.Where(x => x == CSharpLexer.indentDecreaseCharacter).Count(); if (numOpen > numClose) { // add auto-indent closing indentMinusOne = $"\n{indentMinusOne}}}"; InputField.text = InputField.text.Insert(caretPos, indentMinusOne); } // insert the actual auto indent now InputField.text = InputField.text.Insert(caretPos, indent); InputField.stringPosition = caretPos + indent.Length; } } // Update line column and indent positions UpdateIndent(); inputText.text = InputField.text; inputText.SetText(InputField.text, true); inputText.Rebuild(CanvasUpdate.Prelayout); InputField.ForceLabelUpdate(); InputField.Rebuild(CanvasUpdate.Prelayout); OnInputChanged(true); } private string GetAutoIndentTab(int amount) { string tab = string.Empty; for (int i = 0; i < amount; i++) { tab += "\t"; } return tab; } // ============== Theme ============== // private static Color caretColor = new Color32(255, 255, 255, 255); private static Color textColor = new Color32(255, 255, 255, 255); private static Color backgroundColor = new Color32(37, 37, 37, 255); private static Color lineHighlightColor = new Color32(50, 50, 50, 255); private static Color lineNumberBackgroundColor = new Color32(25, 25, 25, 255); private static Color lineNumberTextColor = new Color32(180, 180, 180, 255); private static Color scrollbarColor = new Color32(45, 50, 50, 255); private void ApplyTheme() { var highlightTextRect = inputHighlightText.GetComponent(); highlightTextRect.anchorMin = Vector2.zero; highlightTextRect.anchorMax = Vector2.one; highlightTextRect.offsetMin = Vector2.zero; highlightTextRect.offsetMax = Vector2.zero; InputField.caretColor = caretColor; inputText.color = textColor; inputHighlightText.color = textColor; background.color = backgroundColor; //lineHighlight.color = lineHighlightColor; lineNumberBackground.color = lineNumberBackgroundColor; lineText.color = lineNumberTextColor; scrollbar.color = scrollbarColor; } private bool AllReferencesAssigned() { if (!InputField || !inputText || !inputHighlightText || !lineText || !background || //!lineHighlight || !lineNumberBackground || !scrollbar) { // One or more references are not assigned return false; } return true; } // ========== UI CONSTRUCTION =========== // public void ConstructUI() { ConsolePage.Instance.Content = UIFactory.CreateUIObject("C# Console", MainMenu.Instance.PageViewport); var mainLayout = ConsolePage.Instance.Content.AddComponent(); mainLayout.preferredHeight = 9900; mainLayout.flexibleHeight = 9000; var mainGroup = ConsolePage.Instance.Content.AddComponent(); mainGroup.childControlHeight = true; mainGroup.childControlWidth = true; mainGroup.childForceExpandHeight = true; mainGroup.childForceExpandWidth = true; #region TOP BAR // Main group object var topBarObj = UIFactory.CreateHorizontalGroup(ConsolePage.Instance.Content); LayoutElement topBarLayout = topBarObj.AddComponent(); topBarLayout.minHeight = 50; topBarLayout.flexibleHeight = 0; var topBarGroup = topBarObj.GetComponent(); topBarGroup.padding.left = 30; topBarGroup.padding.right = 30; topBarGroup.padding.top = 8; topBarGroup.padding.bottom = 8; topBarGroup.spacing = 10; topBarGroup.childForceExpandHeight = true; topBarGroup.childForceExpandWidth = true; topBarGroup.childControlWidth = true; topBarGroup.childControlHeight = true; topBarGroup.childAlignment = TextAnchor.LowerCenter; var topBarLabel = UIFactory.CreateLabel(topBarObj, TextAnchor.MiddleLeft); var topBarLabelLayout = topBarLabel.AddComponent(); topBarLabelLayout.preferredWidth = 800; topBarLabelLayout.flexibleWidth = 10; var topBarText = topBarLabel.GetComponent(); topBarText.text = "C# Console"; topBarText.fontSize = 20; // Enable Suggestions toggle var suggestToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle suggestToggle, out Text suggestToggleText); #if CPP suggestToggle.onValueChanged.AddListener(new Action(SuggestToggleCallback)); #else suggestToggle.onValueChanged.AddListener(SuggestToggleCallback); #endif void SuggestToggleCallback(bool val) { ConsolePage.EnableAutocompletes = val; AutoCompleter.Update(); } suggestToggleText.text = "Suggestions"; suggestToggleText.alignment = TextAnchor.UpperLeft; var suggestTextPos = suggestToggleText.transform.localPosition; suggestTextPos.y = -14; suggestToggleText.transform.localPosition = suggestTextPos; var suggestLayout = suggestToggleObj.AddComponent(); suggestLayout.minWidth = 120; suggestLayout.flexibleWidth = 0; var suggestRect = suggestToggleObj.transform.Find("Background"); var suggestPos = suggestRect.localPosition; suggestPos.y = -14; suggestRect.localPosition = suggestPos; // Enable Auto-indent toggle var autoIndentToggleObj = UIFactory.CreateToggle(topBarObj, out Toggle autoIndentToggle, out Text autoIndentToggleText); #if CPP autoIndentToggle.onValueChanged.AddListener(new Action(OnIndentChanged)); #else autoIndentToggle.onValueChanged.AddListener(OnIndentChanged); #endif void OnIndentChanged(bool val) => ConsolePage.EnableAutoIndent = val; autoIndentToggleText.text = "Auto-indent"; autoIndentToggleText.alignment = TextAnchor.UpperLeft; var autoIndentTextPos = autoIndentToggleText.transform.localPosition; autoIndentTextPos.y = -14; autoIndentToggleText.transform.localPosition = autoIndentTextPos; var autoIndentLayout = autoIndentToggleObj.AddComponent(); autoIndentLayout.minWidth = 120; autoIndentLayout.flexibleWidth = 0; var autoIndentRect = autoIndentToggleObj.transform.Find("Background"); suggestPos = autoIndentRect.localPosition; suggestPos.y = -14; autoIndentRect.localPosition = suggestPos; #endregion #region CONSOLE INPUT var consoleBase = UIFactory.CreateUIObject("CodeEditor", ConsolePage.Instance.Content); var consoleLayout = consoleBase.AddComponent(); consoleLayout.preferredHeight = 500; consoleLayout.flexibleHeight = 50; consoleBase.AddComponent(); var mainRect = consoleBase.GetComponent(); mainRect.pivot = Vector2.one * 0.5f; mainRect.anchorMin = Vector2.zero; mainRect.anchorMax = Vector2.one; mainRect.offsetMin = Vector2.zero; mainRect.offsetMax = Vector2.zero; var mainBg = UIFactory.CreateUIObject("MainBackground", consoleBase); var mainBgRect = mainBg.GetComponent(); mainBgRect.pivot = new Vector2(0, 1); mainBgRect.anchorMin = Vector2.zero; mainBgRect.anchorMax = Vector2.one; mainBgRect.offsetMin = Vector2.zero; mainBgRect.offsetMax = Vector2.zero; var mainBgImage = mainBg.AddComponent(); //var lineHighlight = UIFactory.CreateUIObject("LineHighlight", consoleBase); //var lineHighlightRect = lineHighlight.GetComponent(); //lineHighlightRect.pivot = new Vector2(0.5f, 1); //lineHighlightRect.anchorMin = new Vector2(0, 1); //lineHighlightRect.anchorMax = new Vector2(1, 1); //lineHighlightRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 21); //var lineHighlightImage = lineHighlight.GetComponent(); //if (!lineHighlightImage) //{ // lineHighlightImage = lineHighlight.AddComponent(); //} var linesBg = UIFactory.CreateUIObject("LinesBackground", consoleBase); var linesBgRect = linesBg.GetComponent(); linesBgRect.anchorMin = Vector2.zero; linesBgRect.anchorMax = new Vector2(0, 1); linesBgRect.offsetMin = new Vector2(-17.5f, 0); linesBgRect.offsetMax = new Vector2(17.5f, 0); linesBgRect.sizeDelta = new Vector2(65, 0); var linesBgImage = linesBg.AddComponent(); var inputObj = UIFactory.CreateTMPInput(consoleBase); var inputField = inputObj.GetComponent(); inputField.richText = false; inputField.restoreOriginalTextOnEscape = false; var inputRect = inputObj.GetComponent(); inputRect.pivot = new Vector2(0.5f, 0.5f); inputRect.anchorMin = Vector2.zero; inputRect.anchorMax = new Vector2(0.92f, 1); inputRect.offsetMin = new Vector2(20, 0); inputRect.offsetMax = new Vector2(14, 0); inputRect.anchoredPosition = new Vector2(40, 0); var textAreaObj = inputObj.transform.Find("TextArea"); var textAreaRect = textAreaObj.GetComponent(); textAreaRect.pivot = new Vector2(0.5f, 0.5f); textAreaRect.anchorMin = Vector2.zero; textAreaRect.anchorMax = Vector2.one; var mainTextObj = textAreaObj.transform.Find("Text"); var mainTextRect = mainTextObj.GetComponent(); mainTextRect.pivot = new Vector2(0.5f, 0.5f); mainTextRect.anchorMin = Vector2.zero; mainTextRect.anchorMax = Vector2.one; mainTextRect.offsetMin = Vector2.zero; mainTextRect.offsetMax = Vector2.zero; var mainTextInput = mainTextObj.GetComponent(); //mainTextInput.fontSize = 18; var placeHolderText = textAreaObj.transform.Find("Placeholder").GetComponent(); placeHolderText.text = CodeEditor.STARTUP_TEXT; var linesTextObj = UIFactory.CreateUIObject("LinesText", mainTextObj.gameObject); var linesTextRect = linesTextObj.GetComponent(); var linesTextInput = linesTextObj.AddComponent(); linesTextInput.fontSize = 18; var highlightTextObj = UIFactory.CreateUIObject("HighlightText", mainTextObj.gameObject); var highlightTextRect = highlightTextObj.GetComponent(); highlightTextRect.anchorMin = Vector2.zero; highlightTextRect.anchorMax = Vector2.one; highlightTextRect.offsetMin = Vector2.zero; highlightTextRect.offsetMax = Vector2.zero; var highlightTextInput = highlightTextObj.AddComponent(); //highlightTextInput.fontSize = 18; var scroll = UIFactory.CreateScrollbar(consoleBase); var scrollRect = scroll.GetComponent(); scrollRect.anchorMin = new Vector2(1, 0); scrollRect.anchorMax = new Vector2(1, 1); scrollRect.pivot = new Vector2(0.5f, 1); scrollRect.offsetMin = new Vector2(-25f, 0); var scroller = scroll.GetComponent(); scroller.direction = Scrollbar.Direction.TopToBottom; var scrollColors = scroller.colors; scrollColors.normalColor = new Color(0.6f, 0.6f, 0.6f, 1.0f); scroller.colors = scrollColors; var scrollImage = scroll.GetComponent(); var tmpInput = inputObj.GetComponent(); tmpInput.scrollSensitivity = 15; tmpInput.verticalScrollbar = scroller; // set lines text anchors here after UI is fleshed out linesTextRect.pivot = Vector2.zero; linesTextRect.anchorMin = new Vector2(0, 0); linesTextRect.anchorMax = new Vector2(1, 1); linesTextRect.offsetMin = Vector2.zero; linesTextRect.offsetMax = Vector2.zero; linesTextRect.anchoredPosition = new Vector2(-40, 0); tmpInput.GetComponentInChildren().enabled = false; inputObj.GetComponent().enabled = false; #endregion #region COMPILE BUTTON var compileBtnObj = UIFactory.CreateButton(ConsolePage.Instance.Content); var compileBtnLayout = compileBtnObj.AddComponent(); compileBtnLayout.preferredWidth = 80; compileBtnLayout.flexibleWidth = 0; compileBtnLayout.minHeight = 45; compileBtnLayout.flexibleHeight = 0; var compileButton = compileBtnObj.GetComponent