RichTextBox でシンタックスハイライト

# SyntaxHighlight by RichTextBox

System.Windows.Forms.RichTextBox でシンタックスハイライトを実現しようとすると遅すぎて使い物にならない。。
が、少し工夫することで使えるようになる。

- RichTextBox を２つ用意し、一方は表示用、一方は生成用とする


## シンタックスハイライトを行うためのベースクラス

- 生成用 RichTextBox を内部変数として保持
- RTF 形式のテキストを取得するメソッドを用意
- 継承先でハイライトのパターンと色を決定できるように

<details><summary>ソースコード (C#)</summary><div>

```C#:SyntaxHighlighter.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Windows.Forms;

abstract class SyntaxHighlighter : IDisposable
{
	/// <summary>RTF 生成用コントロール</summary>
	private readonly RichTextBox control = new RichTextBox();

	/// <summary>
	/// リソース解放済みフラグを取得します。
	/// </summary>
	public bool IsDisposed { get; private set; }

	/// <summary>
	/// リソースを解放します。
	/// </summary>
	public void Dispose()
	{
		this.Dispose(true);
		GC.SuppressFinalize(this);
	}

	/// <summary>
	/// リソースを解放します。
	/// </summary>
	/// <param name="disposing"></param>
	protected virtual void Dispose(bool disposing)
	{
		if (!this.IsDisposed)
		{
			if (disposing)
			{
				this.control.Dispose();
			}
			this.IsDisposed = true;
		}
	}

	/// <summary>
	/// リッチテキスト形式のテキストを取得します。
	/// </summary>
	/// <param name="text"></param>
	/// <param name="font"></param>
	/// <param name="wideFont">全角文字のフォント</param>
	/// <returns></returns>
	public string GetRtf(string text, Font font, Font wideFont = null)
	{
		this.control.Clear();
		this.control.Text = text;
		if (!string.IsNullOrEmpty(text))
		{
			// 各パターンに一致する文字色を設定
			foreach (var syntax in this.EnumerateSyntaxes())
			{
				foreach (Match m in Regex.Matches(text, syntax.Pattern, RegexOptions.IgnoreCase))
				{
					this.control.Select(m.Index, m.Length);
					this.control.SelectionColor = syntax.Color;
				}
			}

			// フォントを設定
			this.control.SelectAll();
			this.control.SelectionFont = font;
			if (wideFont != null)
			{
				// 全角 (半角以外) のフォントを設定
				foreach (Match m in Regex.Matches(text, @"[^\x01-\x7E]", RegexOptions.IgnoreCase))
				{
					this.control.Select(m.Index, m.Length);
					this.control.SelectionFont = wideFont ?? font;
				}
			}
		}
		return this.control.Rtf;
	}

	/// <summary>
	/// <see cref="Syntax"/> を列挙します。
	/// </summary>
	/// <returns></returns>
	protected abstract IEnumerable<Syntax> EnumerateSyntaxes();

	/// <summary>
	/// ハイライトのパターンと色を表します。
	/// </summary>
	protected struct Syntax
	{
		public Syntax(string pattern, Color color)
		{
			this.Pattern = pattern;
			this.Color = color;
		}
		public string Pattern { get; }
		public Color Color { get; }
	}
}
```
</div></details>


## ハイライトのパターンと色を決定するためのクラス

- 例として、SQL 用の Highlighter (SQL Server 風)
- 表示色を設定できるようにプロパティとして公開
- 予約語は DBMS によって異なるため、外部から設定可能とする

<details><summary>ソースコード (C#)</summary><div>

```C#:SqlHighlighter.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;

class SqlHighlighter : SyntaxHighlighter
{
	/// <summary>予約語の色</summary>
	public Color WordColor { get; set; } = Color.Blue;

	/// <summary>記号の色</summary>
	public Color SignColor { get; set; } = Color.Gray;

	/// <summary>文字列の色</summary>
	public Color StringColor { get; set; } = Color.DarkRed;

	/// <summary>コメントの色</summary>
	public Color CommentColor { get; set; } = Color.Green;

	/// <summary>予約語</summary>
	public string[] Words { get; set; }

	/// <summary>
	/// 予約語を列挙します。
	/// </summary>
	/// <returns></returns>
	private IEnumerable<string> EnumerateWords() => this.Words ?? this.EnumerateDefaultWords();

	private IEnumerable<string> EnumerateDefaultWords()
	{
		yield return "SELECT";
		yield return "FROM";
		yield return "WHERE";
		yield return "AND";
		yield return "OR";
		yield return "DISTINCT";
		yield return "IS NULL";
		yield return "IS NOT NULL";
		yield return "BETWEEN";
		yield return "IN";
		yield return "LIKE";
		yield return "AS";
		yield return "ORDER BY";
		yield return "ASC";
		yield return "DESC";

		yield return "JOIN";
		yield return "INNER";
		yield return "LEFT";
		yield return "RIGHT";
		yield return "FULL";
		yield return "CROSS";
	}

	//
	protected override IEnumerable<Syntax> EnumerateSyntaxes()
	{
		// 予約語
		var words = this.EnumerateWords();
		foreach (var pattern in words.Select(item => this.ToPattern(item, true)))
		{
			yield return new Syntax(pattern, this.WordColor);
		}

		// 記号
		var signs = new[] { ",", ";", ":", "=", "+", "-", "*", "/", "%", "&", "|", "^", "~", "(", ")", "!" };
		foreach (var pattern in signs.Select(item => this.ToPattern(item, false)))
		{
			yield return new Syntax(pattern, this.SignColor);
		}

		// 文字列
		yield return new Syntax(@"'(\.|[^'])*'", this.StringColor);

		// コメント
		yield return new Syntax(@"--.*[$\r\n]*", this.CommentColor);
		yield return new Syntax(@"/\*[\s\S]*?\*/|//.*", this.CommentColor);
	}

	/// <summary>
	/// 単語を正規表現パターンに変換します。
	/// </summary>
	/// <param name="word"></param>
	/// <param name="whole">完全一致フラグ</param>
	/// <returns></returns>
	private string ToPattern(string word, bool whole)
	{
		var pattern = Regex.Escape(word).Replace(" ", @"\s+");
		if (whole) pattern = @"\b" + pattern + @"\b";
		return pattern;
	}
}
```
</div></details>


## 使用例

- テキストが変更される度にハイライトするのは無駄なので、タイマーで処理
- インスタンス生成をタイマー内で行っているが、外に出すべき (例なので。。)

<details><summary>ソースコード (C#)</summary><div>

```C#
// テキスト変更フラグ (テキスト変更時に ON、ハイライト後に OFF)
var textChanged = true;
this.richTextBox.TextChanged += (sender, e) => textChanged = true;

timer1.Tick += (sender, e) =>
{
	if (textChanged)
	{
		// キャレット位置を保持
		var start = this.richTextBox.SelectionStart;
		var length = this.richTextBox.SelectionLength;
		try
		{
			// ハイライト
			using (var highlighter = new SqlHighlighter())
			using (var font = new Font("Consolas", 11))
			{
				richTextBox1.Rtf = highlighter.GetRtf(richTextBox1.Text, font);
			}
		}
		finally
		{
			// キャレット位置を復元
			this.richTextBox.Select(start, length);
		}
		textChanged = false;
	}
}
```
</div></details>


## おまけ

各 DBMS の予約語は、DbConnection クラスの GetSchema メソッドで取得可能

<details><summary>ソースコード (C#)</summary><div>

```C#
using (var connection = new SqlConnection("接続文字列"))
{
	var data = connection.GetSchema("ReservedWords");
	var words = data.Rows.Cast<DataRow>().Select(row => (string)row[0]);
}
```
</div></details>