TextMateSharp
Visual Studio CodeでSyntaxHighlightを行っている技術にTextMateというものがあります。
文法(Grammer
)やテーマ(Theme
)をjson
形式で記述することで、様々な言語のSyntaxHighlightの対応やテーマの対応ができる広く知られたフォーマットになります。
これをC#用にしたのがTextMateSharp
です。
TextMateSharp
を使用することで、C#のフレームワークで簡単にSyntaxHighlightを実装することができます。
サンプルについて
上記のGitHubリンクにサンプルアプリがあるのでこれで解説しようと思います。
必要に応じてGitClone
してください。
TextMateSharp.Demoは以下のようになっています。
using System;
using System.Globalization;
using System.IO;
using TextMateSharp.Grammars;
using TextMateSharp.Themes;
using Spectre.Console;
namespace TextMateSharp
{
class Program
{
static void Main(string[] args)
{
try
{
// コマンドライン引数がないとき、何もしない
if (args.Length < 1)
{
// エラーのみ表示
Console.WriteLine("Usage TextMateSharp.Demo <fileToParse.cs>");
Console.WriteLine("EXAMPLE TextMateSharp.Demo .\\testdata\\samplefiles\\sample.cs");
return;
}
// 表示する文字列を引数から取得
//(今回はコマンドライン引数に".\\testdata\\samplefiles\\sample.cs"を指定して表示するファイルを取得)
string fileToParse = Path.GetFullPath(args[0]);
if (!File.Exists(fileToParse))
{
Console.WriteLine("No such file to parse: {0}", args[0]);
return;
}
// RegistryOptionsを作成
// RegistryOptionは文法やテーマを設定するオプション(引数に使いたい組み込みテーマを設定)
RegistryOptions options = new RegistryOptions(ThemeName.DarkPlus);
Registry.Registry registry = new Registry.Registry(options);
// 設定したテーマをthemeに格納
Theme theme = registry.GetTheme();
int ini = Environment.TickCount;
// 使用する文法を指定したファイルの拡張子から取得(今回はsample.csなのでcsharpの文法を使うという意味)
IGrammar grammar = registry.LoadGrammar(options.GetScopeByExtension(Path.GetExtension(fileToParse)));
if (grammar == null)
{
Console.WriteLine(File.ReadAllText(fileToParse));
return;
}
Console.WriteLine("Grammar loaded in {0}ms.",
Environment.TickCount - ini);
int tokenizeIni = Environment.TickCount;
IStateStack? ruleStack = null;
// 指定したファイルを読み込み、
using (StreamReader sr = new StreamReader(fileToParse))
{
string? line = sr.ReadLine();
// 各行ずつ処理
while (line != null)
{
// 1行を指定した文法でトークン化
ITokenizeLineResult result = grammar.TokenizeLine(line, ruleStack, TimeSpan.MaxValue);
ruleStack = result.RuleStack;
// トークン化された文字に対しての処理
foreach (IToken token in result.Tokens)
{
// トークンの最初の文字
int startIndex = (token.StartIndex > line.Length) ?
line.Length : token.StartIndex;
// トークンの最後の文字
int endIndex = (token.EndIndex > line.Length) ?
line.Length : token.EndIndex;
// 文字の色、背景の色、フォントスタイルを初期化
int foreground = -1;
int background = -1;
FontStyle fontStyle = FontStyle.NotSet;
// トークンに文字色、背景色、フォントスタイルを割り当てる
foreach (var themeRule in theme.Match(token.Scopes))
{
if (foreground == -1 && themeRule.foreground > 0)
foreground = themeRule.foreground;
if (background == -1 && themeRule.background > 0)
background = themeRule.background;
if (fontStyle == FontStyle.NotSet && themeRule.fontStyle > 0)
fontStyle = themeRule.fontStyle;
}
// トークンをコンソールに書き出す
WriteToken(line.SubstringAtIndexes(startIndex, endIndex), foreground, background, fontStyle, theme);
}
Console.WriteLine();
line = sr.ReadLine();
}
}
// 以下の分はTextMateSharpは関係なく、このテーマは何の色ですよと羅列しているだけ
var colorDictionary = theme.GetGuiColorDictionary();
if (colorDictionary is { Count: > 0 })
{
Console.WriteLine("Gui Control Colors");
foreach (var kvp in colorDictionary)
{
Console.WriteLine( $" {kvp.Key}, {kvp.Value}");
}
}
// 処理にかかった時間
Console.WriteLine("File {0} tokenized in {1}ms.",
Path.GetFileName(fileToParse),
Environment.TickCount - tokenizeIni);
}
catch (Exception ex)
{
Console.WriteLine("ERROR: " + ex.Message);
}
}
// トークンを書き出す処理
static void WriteToken(string text, int foreground, int background, FontStyle fontStyle, Theme theme)
{
// 文字色が-1のまま(変化なし)ならばそのままの色でテキストを書き出す
if (foreground == -1)
{
Console.Write(text);
return;
}
// フォントスタイルを設定
Decoration decoration = GetDecoration(fontStyle);
// 背景色を設定
Color backgroundColor = GetColor(background, theme);
// 文字色を設定
Color foregroundColor = GetColor(foreground, theme);
// 文字色、背景色、フォントスタイルをまとめたものをstyleとして保持
Style style = new Style(foregroundColor, backgroundColor, decoration);
// そのstyleで文字を修飾
Markup markup = new Markup(text.Replace("[", "[[").Replace("]", "]]"), style);
// コンソールに文字を修飾して書き込めるライブラリ"AnsiConsole"を使って色のついた文字をコンソールに書き込む
AnsiConsole.Write(markup);
}
// 色をthemeに入っているカラーIDを元に取得
static Color GetColor(int colorId, Theme theme)
{
if (colorId == -1)
return Color.Default;
return HexToColor(theme.GetColor(colorId));
}
// フォントスタイルを取得
static Decoration GetDecoration(FontStyle fontStyle)
{
Decoration result = Decoration.None;
if (fontStyle == FontStyle.NotSet)
return result;
if ((fontStyle & FontStyle.Italic) != 0)
result |= Decoration.Italic;
if ((fontStyle & FontStyle.Underline) != 0)
result |= Decoration.Underline;
if ((fontStyle & FontStyle.Bold) != 0)
result |= Decoration.Bold;
return result;
}
// ColorIDをColorにしている
static Color HexToColor(string hexString)
{
//replace # occurences
if (hexString.IndexOf('#') != -1)
hexString = hexString.Replace("#", "");
byte r, g, b = 0;
r = byte.Parse(hexString.Substring(0, 2), NumberStyles.AllowHexSpecifier);
g = byte.Parse(hexString.Substring(2, 2), NumberStyles.AllowHexSpecifier);
b = byte.Parse(hexString.Substring(4, 2), NumberStyles.AllowHexSpecifier);
return new Color(r, g, b);
}
}
// 文字を加工
internal static class StringExtensions
{
internal static string SubstringAtIndexes(this string str, int startIndex, int endIndex)
{
return str.Substring(startIndex, endIndex - startIndex);
}
}
}
上記を実行するとコンソールに".\testdata\samplefiles\sample.cs"の中身がテーマに沿って色付けされ、表示されます。
TextMateSharpのポイントとして
-
RegistryOptions()
に組み込みのテーマの種類を渡す -
registry.LoadGrammer()
でファイルの拡張子を渡す -
grammer
で文字列をトークン化し、トークンのスコープにより、色を決める - 決めた色をColorに変換して出力する
です。
これを行うことで文字に文法に沿ったテーマで色を付けることができます。
外部から文法やテーマを取り込む
TextMateSharpで色を付けることができましたが、現在組み込まれている文法、テーマにのみ対応しています。
Visual Studio Codeのように外部からjsonを追加し様々な文法やテーマに対応したいと思われるでしょう。
その対応について紹介します。
本来であれば、TextMateSharp.Grammer
のRegistryOption
を参考にIRegistryOption
を実装する必要があるのですが、今回は手抜きをして簡単に外部から文法やテーマを与える方法を教えます。
文法は以下の通りです。
// RegistryOptionsを継承,上書きできるようIRegistryOptionsも実装
class GrammerRegistryOptions : RegistryOptions, IRegistryOptions
{
// 継承元の`RegistryOptions`のコンストラクタを呼び出す
public GrammerRegistryOptions(ThemeName defaultTheme) : base(defaultTheme)
{
}
// 修飾子を`new`にし`GetGrammar`を上書き
public new IRawGrammar GetGrammar(string scopeName)
{
// 追加したい外部のTextMate文法ファイルを取得する
string grammerPath = Path.GetFullPath(
@"C:\Users\isisg\source\repos\newlanguage-syntax-grammer.json");
using (StreamReader reader = new StreamReader(grammerPath))
{
// 文法を外部ファイルのものとする
return GrammarReader.ReadGrammarSync(reader);
}
}
}
テーマは以下の通りです。
// RegistryOptionsを継承,上書きできるようIRegistryOptionsも実装
class ThemeRegistryOptions : RegistryOptions, IRegistryOptions
{
// 継承元の`RegistryOptions`のコンストラクタを呼び出す
public ThemeRegistryOptions(ThemeName defaultTheme) : base(defaultTheme)
{
}
// 修飾子を`new`にし`GetTheme`を上書き
public new IRawTheme GetTheme(string scopeName)
{
// 追加したい外部のTextMateテーマファイルを取得する
string themePath = Path.GetFullPath(
@"C:\Users\isisg\source\repos\monokai-color-theme.json");
using (StreamReader reader = new StreamReader(themePath))
{
// 文法を外部ファイルのものとする
return ThemeReader.ReadThemeSync(reader);
}
}
}
そしてGrammerを使うときですが、GetExtension
が使用できません。
指定したGrammerの拡張子をgrammerのjsonファイルに書いてあるのでそれを使用します。
例えば以下のようなgrammerのjsonでは
"information_for_contributors": [
"This file has been converted from https://github.com/dotnet/csharp-tmLanguage/blob/master/grammars/csharp.tmLanguage",
"If you want to provide a fix or improvement, please create a pull request against the original repository.",
"Once accepted there, we are happy to receive an update request."
],
"version": "https://github.com/dotnet/csharp-tmLanguage/commit/7a7482ffc72a6677a87eb1ed76005593a4f7f131",
"name": "New",
+ "scopeName": "source.new",
"patterns": [
{,,,
の"scopeName"を確認し、
LoadGrammer
の部分で以下のように指定します。
Registry.Registry registry = new Registry.Registry(options);
Theme theme = registry.GetTheme();
int ini = Environment.TickCount;
+ IGrammar grammar = registry.LoadGrammar("source.new");
if (grammar == null)
{
Console.WriteLine(File.ReadAllText(fileToParse));
return;
}
LoadGrammer
の引数に"scopeName"を合わせる形になります。
これで、デフォルトの文法やテーマを維持しながら、外部から文法やテーマを取り込むことができるようになります。