Files
2026-10-24 13:02:19 +08:00

203 lines
8.0 KiB
C#

using System.Text.RegularExpressions;
using anjianjingling.Native;
namespace anjianjingling.Forms
{
/// <summary>
/// Lua 脚本编辑器:行号栏 + 语法着色(关键字/字符串/注释/数字)。
/// 纯 WinForms,无第三方依赖。
/// </summary>
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);
}
/// <summary>脚本文本(不用基类 Text,避免与控件标题混淆)。</summary>
[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<int, int>? 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; }
}
}
}