using System.Text.RegularExpressions; using anjianjingling.Native; namespace anjianjingling.Forms { /// /// Lua 脚本编辑器:行号栏 + 语法着色(关键字/字符串/注释/数字)。 /// 纯 WinForms,无第三方依赖。 /// public sealed class LuaEditor : UserControl { private readonly RichTextBox _rtb = new(); private readonly Panel _gutter = new(); private readonly System.Windows.Forms.Timer _hlTimer = new(); private bool _highlighting; private const int WM_SETREDRAW = 0x000B; private const int WM_USER = 0x0400; private const int EM_GETFIRSTVISIBLELINE = WM_USER + 206; [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); public LuaEditor() { _gutter.Dock = DockStyle.Left; _gutter.Width = 46; _gutter.BackColor = Color.FromArgb(240, 243, 248); _gutter.Paint += GutterPaint; _rtb.Dock = DockStyle.Fill; _rtb.BorderStyle = BorderStyle.None; _rtb.Font = new Font("Consolas", 11F); _rtb.BackColor = Color.FromArgb(252, 253, 255); _rtb.ForeColor = Color.FromArgb(30, 34, 42); _rtb.WordWrap = false; _rtb.AcceptsTab = true; _rtb.ScrollBars = RichTextBoxScrollBars.Both; _rtb.DetectUrls = false; _rtb.HideSelection = false; _rtb.VScroll += (s, e) => _gutter.Invalidate(); _rtb.HScroll += (s, e) => _gutter.Invalidate(); _rtb.TextChanged += (s, e) => { _hlTimer.Stop(); _hlTimer.Start(); ScriptChanged?.Invoke(this, EventArgs.Empty); _gutter.Invalidate(); }; _rtb.SelectionChanged += (s, e) => CaretMoved?.Invoke(LineOf(_rtb.SelectionStart), ColumnOf(_rtb.SelectionStart)); _rtb.KeyDown += (s, e) => { if (e.Control && e.KeyCode == Keys.A) { _rtb.SelectAll(); e.SuppressKeyPress = true; } // Tab 插入 4 空格而不是制表符 if (e.KeyCode == Keys.Tab && !e.Shift && !e.Control) { _rtb.SelectedText = " "; e.SuppressKeyPress = true; } }; _hlTimer.Interval = 180; _hlTimer.Tick += (s, e) => { _hlTimer.Stop(); Highlight(); }; Controls.Add(_rtb); Controls.Add(_gutter); } /// 脚本文本(不用基类 Text,避免与控件标题混淆)。 [System.ComponentModel.Browsable(false)] [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)] public string Script { get => _rtb.Text; set { _rtb.Text = value; Highlight(); } } public RichTextBox Inner => _rtb; public event EventHandler? ScriptChanged; public event Action? CaretMoved; public void FocusEditor() => _rtb.Focus(); public int CurrentLine => LineOf(_rtb.SelectionStart); public void GotoLine(int line) { var lines = _rtb.Lines; if (line < 0 || line >= lines.Length) return; int idx = 0; for (int i = 0; i < line; i++) idx += lines[i].Length + 1; _rtb.SelectionStart = idx; _rtb.SelectionLength = 0; _rtb.ScrollToCaret(); _rtb.Focus(); } private int LineOf(int charIndex) { int line = _rtb.GetLineFromCharIndex(charIndex); return line + 1; } private int ColumnOf(int charIndex) { int first = _rtb.GetFirstCharIndexFromLine(_rtb.GetLineFromCharIndex(charIndex)); return charIndex - first + 1; } // ===================== 行号 ===================== private void GutterPaint(object? sender, PaintEventArgs e) { var g = e.Graphics; g.Clear(_gutter.BackColor); using var font = new Font("Consolas", 9.5F); using var brush = new SolidBrush(Color.FromArgb(130, 142, 162)); using var curBrush = new SolidBrush(Color.FromArgb(37, 99, 235)); int firstVisible = (int)SendMessage(_rtb.Handle, EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero); int lineCount = _rtb.Lines.Length; int lineHeight = _rtb.Font.Height; int curLine = _rtb.GetLineFromCharIndex(_rtb.SelectionStart); for (int i = 0; i < 200; i++) { int lineIdx = firstVisible + i; if (lineIdx >= lineCount) break; int y = i * lineHeight + 2; if (y > _gutter.Height) break; string num = (lineIdx + 1).ToString(); var sz = g.MeasureString(num, font); var b = lineIdx == curLine ? curBrush : brush; g.DrawString(num, font, b, _gutter.Width - sz.Width - 8, y); } } // ===================== 语法着色 ===================== private static readonly Regex RxComment = new(@"--\[\[.*?\]\]|--[^\n]*", RegexOptions.Compiled | RegexOptions.Singleline); private static readonly Regex RxString = new("\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'", RegexOptions.Compiled); private static readonly Regex RxNumber = new(@"\b0x[0-9a-fA-F]+\b|\b\d+(\.\d+)?\b", RegexOptions.Compiled); private static readonly Regex RxKeyword = new( @"\b(and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b", RegexOptions.Compiled); private static readonly Regex RxApi = new( @"\b(delay|delayRandom|moveTo|moveR|leftClick|rightClick|middleClick|doubleClick|wheel|drag|keyPress|keyDown|keyUp|hotkey|typeText|sayText|findImage|findColor|waitImage|exists|screenshot|colorAt|clipGet|clipSet|run|activate|log|stop|random|now|sleep|mouse|key|screen|win|sys|clip|browser)\b", RegexOptions.Compiled); private static readonly Color CKeyword = Color.FromArgb(175, 45, 130); private static readonly Color CString = Color.FromArgb(20, 130, 80); private static readonly Color CComment = Color.FromArgb(140, 150, 165); private static readonly Color CNumber = Color.FromArgb(198, 106, 22); private static readonly Color CApi = Color.FromArgb(30, 90, 200); private static readonly Color CPlain = Color.FromArgb(30, 34, 42); public void Highlight() { if (_highlighting || _rtb.TextLength == 0) return; _highlighting = true; try { string text = _rtb.Text; int selStart = _rtb.SelectionStart; int selLen = _rtb.SelectionLength; SendMessage(_rtb.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero); _rtb.SelectAll(); _rtb.SelectionColor = CPlain; void Paint(Regex rx, Color color) { foreach (Match m in rx.Matches(text)) { if (m.Length == 0) continue; _rtb.Select(m.Index, m.Length); _rtb.SelectionColor = color; } } Paint(RxNumber, CNumber); Paint(RxKeyword, CKeyword); Paint(RxApi, CApi); Paint(RxString, CString); Paint(RxComment, CComment); _rtb.Select(selStart, selLen); _rtb.SelectionColor = CPlain; SendMessage(_rtb.Handle, WM_SETREDRAW, (IntPtr)1, IntPtr.Zero); _rtb.Invalidate(); } catch { } finally { _highlighting = false; } } } }