diff --git a/src/UI/CSConsole/ConsoleController.cs b/src/UI/CSConsole/ConsoleController.cs index aba3c4d..3c88e1b 100644 --- a/src/UI/CSConsole/ConsoleController.cs +++ b/src/UI/CSConsole/ConsoleController.cs @@ -36,6 +36,8 @@ namespace UnityExplorer.UI.CSConsole public static bool EnableAutoIndent { get; private set; } = true; public static bool EnableSuggestions { get; private set; } = true; + internal static string ScriptsFolder => Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Scripts"); + internal static readonly string[] DefaultUsing = new string[] { "System", @@ -56,6 +58,18 @@ namespace UnityExplorer.UI.CSConsole ResetConsole(false); // ensure the compiler is supported (if this fails then SRE is probably stubbed) Evaluator.Compile("0 == 0"); + + if (!Directory.Exists(ScriptsFolder)) + Directory.CreateDirectory(ScriptsFolder); + + var startupPath = Path.Combine(ScriptsFolder, "startup.cs"); + if (File.Exists(startupPath)) + { + ExplorerCore.Log($"Executing startup script from '{startupPath}'..."); + var text = File.ReadAllText(startupPath); + Input.Text = text; + Evaluate(); + } } catch (Exception ex) { @@ -69,7 +83,7 @@ namespace UnityExplorer.UI.CSConsole SetupHelpInteraction(); Panel.OnInputChanged += OnInputChanged; - Panel.InputScroll.OnScroll += OnInputScrolled; + Panel.InputScroller.OnScroll += OnInputScrolled; Panel.OnCompileClicked += Evaluate; Panel.OnResetClicked += ResetConsole; Panel.OnHelpDropdownChanged += HelpSelected; @@ -317,7 +331,7 @@ namespace UnityExplorer.UI.CSConsole var charBot = charTop - CSCONSOLE_LINEHEIGHT; var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); - var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; + var viewportMax = viewportMin - Panel.InputScroller.ViewportRect.rect.height; float diff = 0f; if (charTop > viewportMin) @@ -337,7 +351,7 @@ namespace UnityExplorer.UI.CSConsole { settingCaretCoroutine = true; Input.Component.readOnly = true; - RuntimeProvider.Instance.StartCoroutine(SetAutocompleteCaretCoro(caretPosition)); + RuntimeProvider.Instance.StartCoroutine(SetCaretCoroutine(caretPosition)); } internal static PropertyInfo SelectionGuardProperty => selectionGuardPropInfo ?? GetSelectionGuardPropInfo(); @@ -352,7 +366,7 @@ namespace UnityExplorer.UI.CSConsole private static PropertyInfo selectionGuardPropInfo; - private static IEnumerator SetAutocompleteCaretCoro(int caretPosition) + private static IEnumerator SetCaretCoroutine(int caretPosition) { var color = Input.Component.selectionColor; color.a = 0f; @@ -376,7 +390,6 @@ namespace UnityExplorer.UI.CSConsole settingCaretCoroutine = false; } - #region Lexer Highlighting /// @@ -384,43 +397,83 @@ namespace UnityExplorer.UI.CSConsole /// private static bool HighlightVisibleInput() { - int startIdx = 0; - int endIdx = Input.Text.Length - 1; - int topLine = 0; - - // Calculate visible text if necessary - if (Input.Rect.rect.height > Panel.InputScroll.ViewportRect.rect.height) + if (string.IsNullOrEmpty(Input.Text)) { - topLine = -1; - int bottomLine = -1; - - // the top and bottom position of the viewport in relation to the text height - // they need the half-height adjustment to normalize against the 'line.topY' value. - var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); - var viewportMax = viewportMin - Panel.InputScroll.ViewportRect.rect.height; - - for (int i = 0; i < Input.TextGenerator.lineCount; i++) - { - var line = Input.TextGenerator.lines[i]; - // if not set the top line yet, and top of line is below the viewport top - if (topLine == -1 && line.topY <= viewportMin) - topLine = i; - // if bottom of line is below the viewport bottom - if ((line.topY - line.height) >= viewportMax) - bottomLine = i; - } - - topLine = Math.Max(0, topLine - 1); - bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1); - - startIdx = Input.TextGenerator.lines[topLine].startCharIdx; - endIdx = (bottomLine >= Input.TextGenerator.lineCount - 1) - ? Input.Text.Length - 1 - : (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1); + Panel.HighlightText.text = ""; + Panel.LineNumberText.text = "1"; + return false; } + // Calculate the visible lines + + int topLine = -1; + int bottomLine = -1; + + // the top and bottom position of the viewport in relation to the text height + // they need the half-height adjustment to normalize against the 'line.topY' value. + var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f); + var viewportMax = viewportMin - Panel.InputScroller.ViewportRect.rect.height; + + for (int i = 0; i < Input.TextGenerator.lineCount; i++) + { + var line = Input.TextGenerator.lines[i]; + // if not set the top line yet, and top of line is below the viewport top + if (topLine == -1 && line.topY <= viewportMin) + topLine = i; + // if bottom of line is below the viewport bottom + if ((line.topY - line.height) >= viewportMax) + bottomLine = i; + } + + topLine = Math.Max(0, topLine - 1); + bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1); + + int startIdx = Input.TextGenerator.lines[topLine].startCharIdx; + int endIdx = (bottomLine >= Input.TextGenerator.lineCount - 1) + ? Input.Text.Length - 1 + : (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1); + + // Highlight the visible text with the LexerBuilder + Panel.HighlightText.text = Lexer.BuildHighlightedString(Input.Text, startIdx, endIdx, topLine, LastCaretPosition, out bool ret); + + // Set the line numbers + + // determine true starting line number (not the same as the cached TextGenerator line numbers) + int realStartLine = 0; + for (int i = 0; i < startIdx; i++) + { + if (LexerBuilder.IsNewLine(Input.Text[i])) + realStartLine++; + } + realStartLine++; + char lastPrev = '\n'; + + var sb = new StringBuilder(); + + // append leading new lines for spacing (no point rendering line numbers we cant see) + for (int i = 0; i < topLine; i++) + sb.Append('\n'); + + // append the displayed line numbers + for (int i = topLine; i <= bottomLine; i++) + { + if (i > 0) + lastPrev = Input.Text[Input.TextGenerator.lines[i].startCharIdx - 1]; + + // previous line ended with a newline character, this is an actual new line. + if (LexerBuilder.IsNewLine(lastPrev)) + { + sb.Append(realStartLine.ToString()); + realStartLine++; + } + + sb.Append('\n'); + } + + Panel.LineNumberText.text = sb.ToString(); + return ret; } diff --git a/src/UI/CSConsole/LexerBuilder.cs b/src/UI/CSConsole/LexerBuilder.cs index 9e18997..4bf19d2 100644 --- a/src/UI/CSConsole/LexerBuilder.cs +++ b/src/UI/CSConsole/LexerBuilder.cs @@ -13,8 +13,9 @@ namespace UnityExplorer.UI.CSConsole { public int startIndex; public int endIndex; - public string htmlColorTag; public bool isStringOrComment; + public bool matchToEndOfLine; + public string htmlColorTag; } public class LexerBuilder @@ -112,12 +113,24 @@ namespace UnityExplorer.UI.CSConsole sb.Append(input[i]); sb.Append(SignatureHighlighter.CLOSE_COLOR); - // check caretIdx to determine inStringOrComment state - if (caretIdx >= match.startIndex && (caretIdx <= match.endIndex || (caretIdx >= input.Length && match.endIndex >= input.Length - 1))) - caretInStringOrComment = match.isStringOrComment; - // update the last unhighlighted start index lastUnhighlighted = match.endIndex + 1; + + int matchEndIdx = match.endIndex; + if (match.matchToEndOfLine) + { + while (input.Length - 1 >= matchEndIdx) + { + if (IsNewLine(input[matchEndIdx])) + break; + matchEndIdx++; + } + } + + // check caretIdx to determine inStringOrComment state + if (caretIdx >= match.startIndex && (caretIdx <= matchEndIdx || (caretIdx >= input.Length && matchEndIdx >= input.Length - 1))) + caretInStringOrComment = match.isStringOrComment; + } // Append trailing unhighlighted input diff --git a/src/UI/Panels/CSConsolePanel.cs b/src/UI/Panels/CSConsolePanel.cs index a6a7846..0d21aaa 100644 --- a/src/UI/Panels/CSConsolePanel.cs +++ b/src/UI/Panels/CSConsolePanel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -18,10 +19,11 @@ namespace UnityExplorer.UI.Panels public override int MinWidth => 750; public override int MinHeight => 300; - public InputFieldScroller InputScroll { get; private set; } - public InputFieldRef Input => InputScroll.InputField; + public InputFieldScroller InputScroller { get; private set; } + public InputFieldRef Input => InputScroller.InputField; public Text InputText { get; private set; } public Text HighlightText { get; private set; } + public Text LineNumberText { get; private set; } public Dropdown HelpDropdown { get; private set; } @@ -121,19 +123,53 @@ namespace UnityExplorer.UI.Panels // Console Input + var inputArea = UIFactory.CreateUIObject("InputGroup", content); + UIFactory.SetLayoutElement(inputArea, flexibleWidth: 9999, flexibleHeight: 9999); + UIFactory.SetLayoutGroup(inputArea, false, true, true, true); + inputArea.AddComponent().color = Color.white; + inputArea.AddComponent().showMaskGraphic = false; + + // line numbers + + var linesHolder = UIFactory.CreateUIObject("LinesHolder", inputArea); + var linesRect = linesHolder.GetComponent(); + linesRect.pivot = new Vector2(0, 1); + linesRect.anchorMin = new Vector2(0, 0); + linesRect.anchorMax = new Vector2(0, 1); + linesRect.sizeDelta = new Vector2(0, 305000); + linesRect.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 50); + linesHolder.AddComponent().color = new Color(0.05f, 0.05f, 0.05f); + UIFactory.SetLayoutGroup(linesHolder, true, true, true, true); + + LineNumberText = UIFactory.CreateLabel(linesHolder, "LineNumbers", "1", TextAnchor.UpperCenter, Color.grey, fontSize: 16); + LineNumberText.font = UIManager.ConsoleFont; + + // input field + int fontSize = 16; - var inputObj = UIFactory.CreateScrollInputField(this.content, "ConsoleInput", ConsoleController.STARTUP_TEXT, out var inputScroller, fontSize); - InputScroll = inputScroller; + var inputObj = UIFactory.CreateScrollInputField(inputArea, "ConsoleInput", ConsoleController.STARTUP_TEXT, + out var inputScroller, fontSize); + InputScroller = inputScroller; ConsoleController.defaultInputFieldAlpha = Input.Component.selectionColor.a; Input.OnValueChanged += InvokeOnValueChanged; + // move line number text with input field + linesRect.transform.SetParent(inputObj.transform.Find("Viewport"), false); + inputScroller.Slider.Scrollbar.onValueChanged.AddListener((float val) => { SetLinesPosition(); }); + inputScroller.Slider.Slider.onValueChanged.AddListener((float val) => { SetLinesPosition(); }); + void SetLinesPosition() + { + linesRect.anchoredPosition = new Vector2(linesRect.anchoredPosition.x, inputScroller.ContentRect.anchoredPosition.y); + //SetInputLayout(); + } + InputText = Input.Component.textComponent; InputText.supportRichText = false; - Input.PlaceholderText.fontSize = fontSize; InputText.color = Color.clear; Input.Component.customCaretColor = true; Input.Component.caretColor = Color.white; + Input.PlaceholderText.fontSize = fontSize; // Lexer highlight text overlay var highlightTextObj = UIFactory.CreateUIObject("HighlightText", InputText.gameObject); @@ -154,7 +190,19 @@ namespace UnityExplorer.UI.Panels Input.PlaceholderText.font = UIManager.ConsoleFont; HighlightText.font = UIManager.ConsoleFont; + RuntimeProvider.Instance.StartCoroutine(DelayedLayoutSetup()); + } + private IEnumerator DelayedLayoutSetup() + { + yield return null; + SetInputLayout(); + } + + public void SetInputLayout() + { + Input.Rect.offsetMin = new Vector2(52, Input.Rect.offsetMin.y); + Input.Rect.offsetMax = new Vector2(2, Input.Rect.offsetMax.y); } } } diff --git a/src/UI/Widgets/InputFieldScroller.cs b/src/UI/Widgets/InputFieldScroller.cs index 4fea90c..5233e7f 100644 --- a/src/UI/Widgets/InputFieldScroller.cs +++ b/src/UI/Widgets/InputFieldScroller.cs @@ -80,12 +80,12 @@ namespace UnityExplorer.UI.Widgets if (ContentRect.rect.height < desiredHeight) { - ContentRect.sizeDelta = new Vector2(0, desiredHeight); + ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight); this.Slider.UpdateSliderHandle(); } else if (ContentRect.rect.height > desiredHeight) { - ContentRect.sizeDelta = new Vector2(0, desiredHeight); + ContentRect.sizeDelta = new Vector2(ContentRect.sizeDelta.x, desiredHeight); this.Slider.UpdateSliderHandle(); } }