203 lines
8.0 KiB
C#
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; }
|
|
}
|
|
}
|
|
}
|