0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 10

TextMateSharpについての紹介

Last updated at Posted at 2024-12-09

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のポイントとして

  1. RegistryOptions()に組み込みのテーマの種類を渡す
  2. registry.LoadGrammer()でファイルの拡張子を渡す
  3. grammerで文字列をトークン化し、トークンのスコープにより、色を決める
  4. 決めた色をColorに変換して出力する

です。
これを行うことで文字に文法に沿ったテーマで色を付けることができます。

外部から文法やテーマを取り込む

TextMateSharpで色を付けることができましたが、現在組み込まれている文法、テーマにのみ対応しています。
Visual Studio Codeのように外部からjsonを追加し様々な文法やテーマに対応したいと思われるでしょう。
その対応について紹介します。

本来であれば、TextMateSharp.GrammerRegistryOptionを参考に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"を合わせる形になります。

これで、デフォルトの文法やテーマを維持しながら、外部から文法やテーマを取り込むことができるようになります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?