はじめに
uRaymarching を作る上でシンタックスハイライト付きのコードエディタが欲しくて色々試してみましたので、その内容を共有します。
デモ
こんな感じのシンタックスハイライト付きテキストエリアが作れます。
カラーコードを意識したりフォントを変更すると味わい深くなります。
方針
はじめは以下のエントリを参考にしようと考えていました。
このコードでは、背後に透明なテキストで TextArea
を描画した上に背景が透明で単語がシンタックスハイライトされた TextField
を描画する形式でした。具体的には、単語の位置やキーコントロールなど内部情報を持っている TextEditor
クラスのインスタンスを GUIUtility.GetStateObject()
で取得して、それを通じて単語の位置を取得し、該当箇所にシンタックスハイライトさせた TextField
を描画する、という形です。
ただ、もっと簡単に出来るんじゃないかなと思って改造したのが、上に載せるシンタックスハイライト用の TextArea
を用意して 2 枚の TextArea
が重なっているような形にする方法です。下側が編集用の透明エリア(背景は不透明、文字が透明)で上記コードと同じなのですが、上側は表示用の色付きエリア(背景は透明、文字が色付き)という構成にし、シンタックスハイライトはリッチテキストで表現する、という方法です。
ちなみにリッチテキストだけした文字を表示するとキャレットの位置が変になってしまうという問題と、hoge と見えている文字の e の後ろにキャレットがあるときに一文字消すと <color=red>hoge</color
という感じでシンタックスハイライト用のコードも直接見えてしまうという問題があって、2 枚重ねる必要があります。
コード
では具体的にコードを見てみます。まず、次のようなエディタ部分のコードを書いてみます。
using UnityEngine; using UnityEditor; public class CodeEditor { public Color backgroundColor { get; set; } public Color textColor { get; set; } // ハイライト用の関数 public System.Func<string, string> highlighter { get; set; } // 表示高速化の為に変更があった時だけコードを更新 string cachedHighlightedCode { get; set; } public CodeEditor() { backgroundColor = Color.black; textColor = Color.white; highlighter = code => code; } public string Draw(string code, GUIStyle style, params GUILayoutOption[] options) { // 現在の色を保存 var preBackgroundColor = GUI.backgroundColor; var preColor = GUI.color; // 文字を透明にする var backStyle = new GUIStyle(style); backStyle.normal.textColor = Color.clear; backStyle.hover.textColor = Color.clear; backStyle.active.textColor = Color.clear; backStyle.focused.textColor = Color.clear; // 背景を色付きにする GUI.backgroundColor = backgroundColor; // 編集用のテキストエリアを描画 var editedCode = EditorGUILayout.TextArea(code, backStyle, options); // シンタックスハイライトさせたコードを更新 if (string.IsNullOrEmpty(cachedHighlightedCode) || (editedCode != code)) { cachedHighlightedCode = highlighter(editedCode); } // 背景を透明にする GUI.backgroundColor = Color.clear; // 文字(シンタックスハイライトされない部分)を指定色にする var foreStyle = new GUIStyle(style); foreStyle.normal.textColor = textColor; foreStyle.hover.textColor = textColor; foreStyle.active.textColor = textColor; foreStyle.focused.textColor = textColor; // リッチテキストを ON にする foreStyle.richText = true; // シンタックスハイライト用のテキストエリアを表示 EditorGUI.TextArea(GUILayoutUtility.GetLastRect(), cachedHighlightedCode, foreStyle); // 色を元に戻す GUI.backgroundColor = preBackgroundColor; GUI.color = preColor; return editedCode; } }
highlighter
という関数でシンタックスハイライト用のコードを生成して上に重ねて表示しています。また、毎回シンタックスハイライトしてると highlighter
の処理が重いときにパフォーマンスが落ちるので、必要がある時のみ更新しているのがミソです。その為、通常の TextArea
のように関数にすることはできず、状態を保存するためにクラスにする必要があります。
次に利用側を見てみます。
using UnityEngine; using UnityEditor; [CustomEditor(typeof(Test))] public class TestEditor : Editor { CodeEditor editor; Vector2 scrollPos; string code = ""; void OnEnable() { editor = new CodeEditor(); editor.backgroundColor = Color.blue; editor.textColor = Color.white; editor.highlighter = Highlight; } public override void OnInspectorGUI() { var style = new GUIStyle(GUI.skin.textArea); style.padding = new RectOffset(6, 6, 6, 6); style.fontSize = 12; style.wordWrap = false; scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(200)); code = editor.Draw(code, style, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); } string Highlight(string code) { return code.Replace("float", "<color=red>float</color>"); } }
float
という文字だけ赤くするようにしています。
highlighter
を正規表現で処理すれば複雑なシンタックスも簡単に処理できるようになります。次のような Syntax.Highlight()
を用意して highlighter
に登録してみます。これで冒頭のデモになります。
using System.Collections.Generic; using System.Text.RegularExpressions; public static class Syntax { static Regex regex; static MatchEvaluator evaluator; [UnityEditor.InitializeOnLoadMethod] private static void Init() { var type = @"(float|int|double|void)"; var symbol = @"[{}()=;,+\-*/<>|]+"; var digit = @"(?<![a-zA-Z_])[+-]?[0-9]+\.?[0-9]?(([eE][+-]?)?[0-9]+)?"; var comment = @"/\*[\s\S]*?\*/|//.*"; var block = "(?<{0}>({1}))"; var pattern = "(" + string.Join("|", new string[] { string.Format(block, "comment", comment), string.Format(block, "type", type), string.Format(block, "symbol", symbol), string.Format(block, "digit", digit), }) + ")"; regex = new Regex(pattern, RegexOptions.Compiled); var colorTable = new Dictionary<string, string>() { { "type", "#ff0000" }, { "symbol", "#ff00ff" }, { "digit", "#00ff00" }, { "comment", "#555555" }, }; evaluator = new MatchEvaluator(match => { foreach (var pair in colorTable) { if (match.Groups[pair.Key].Success) { return string.Format("<color={1}>{0}</color>", match.Value, pair.Value); } } return match.Value; }); } public static string Highlight(string code) { return regex.Replace(code, evaluator); } }
パフォーマンス改善のために Regex
と MatchEvaluator
はキャッシュしています。より複雑な例(2個めのデモ)は次のように書いています。
- uRaymarching/ShaderSyntax.cs at a99dcb7a4dcee8a7ae6408a7e962366fbc5249cc · hecomi/uRaymarching · GitHub
- uRaymarching/ShaderCodeEditor.cs at a99dcb7a4dcee8a7ae6408a7e962366fbc5249cc · hecomi/uRaymarching · GitHub
問題点
本当は uREPL でやったようにキーバインドつきのエディタを作成したかったのですが、ダメでした...。というのも、方針の項で述べた TextArea
は GUI.TextArea
で、機能が最低限しか実装されておらず、Ctrl + C、Ctrl + V によるコピペといったキー操作が出来なかったりします。GUILayout.TextArea
も同じで、こちらは Tab
キーが奪われてしまう分更に良くありません。しかしながら EditorGUI
系の EditorGUI.TextArea
または EditorGUILayout.TextArea
だと、今度はキー操作は出来る反面、TextEditor
クラスが取得できない(正確には取得できるが内部に何ら情報が格納されていない)状態になってしまいます。
これらを改善するには、GUI.TextArea
で自前でキー操作を実装する必要があるのですが、面倒なので未だ手を付けていないです。元気があるときにヤッてみようと思います。
おわりに
なくても困らない機能ですが、あるとミスを防げたり見た目にもかっこいいのでおすすめです。