DevToysの拡張機能を作ってみた

# DevToysとは？

DevToysは、開発者向けの多機能ツールセットで、「開発者のためのスイスアーミーナイフ」とも呼ばれています。日常の開発作業を効率化するために設計されており、以下のような機能が含まれています。

- JSONの整形や比較
- テキストの差分確認
- 正規表現のテスト
- エンコード・デコードツール（例えば、Base64やURLエンコード）
- 画像の圧縮や変換
- パスワードやハッシュの生成

https://devtoys.app/

WindowsオンリーだったDevToysですが、バージョン2.0でクロスプラットフォームになりMacやLinuxでも利用できるようになりました。また、バージョン2.0では拡張機能によりユーザー自身が独自のツールを開発して追加できるようになりました。

https://qiita.com/danishi/items/a75ab904da66cecc71b4

## DevToys 2.0の実装について
DevToys 2.0 ではクロスプラットフォームのデスクトップアプリという難題をどのように実現しているか？というのもC#erとしてはとても気になるところです。

クロスプラットフォームを実現するために、[Blazor Hybrid](https://learn.microsoft.com/ja-jp/aspnet/core/blazor/hybrid/?view=aspnetcore-9.0)を採用したとの事ですが、なぜBlazor Hybridなのか？
そのあたりの話が、DevToys公式サイトの[ブログ](https://devtoys.app/blog/the-journey-to-devtoys-2.0)や、下記の動画などで語られています。非常に興味深い内容ですので是非チェックしてみて下さい。

<iframe width="560" height="315" src="https://www.youtube.com/embed/8yM4jDooWcM?si=-4jmYYTqwPUo-4f7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<iframe width="560" height="315" src="https://www.youtube.com/embed/W1DFK0wawwM?si=qCW7OcQdpoxRwjmY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


# 拡張機能を作ってみよう
今回、DevToys 2.0で可能になった拡張機能の開発を実際に試してみましたので、ご紹介したいと思います。

## 拡張機能の作り方
拡張機能の作成方法は下記公式ドキュメントに詳しく記載されています。

https://devtoys.app/doc/articles/introduction.html

[Create a minnimal Extension](https://devtoys.app/doc/articles/extension-development/getting-started/create-an-extension.html?tabs=vs)の手順に従っていれば問題なく始められると思います。


## 拡張機能（GUIツール）実装のポイント
DevToysの拡張機能を実装するうえでのポイントをまとめました。

:::note info
拡張機能はGUIツールとCUIツールの２種類が作成可能ですが、ここではGUIツールの場合のみを記載します。
:::

### Managed Extensibility Framework(MEF)
拡張機能は、[Managed Extensibility Framework(MEF)](https://learn.microsoft.com/ja-jp/dotnet/framework/mef/)の仕組みで動いており、下記のようなクラスを定義することでDevToysのGUIプラグインとして認識されます。
- `IGuiTool`を継承
- `Export`属性 `[Export(typeof(IGuiTool))]`を付与
- 追加の属性（`Name`、`ToolDisplayInformation`）でDevToys側に表示する拡張機能の情報を設定する

```cs
using DevToys.Api;
using System.ComponentModel.Composition;
using static DevToys.Api.GUI;

namespace MyExtension;

[Export(typeof(IGuiTool))]
[Name("MyExtension")]                                                         // A unique, internal name of the tool.
[ToolDisplayInformation(
    IconFontName = "FluentSystemIcons",                                       // This font is available by default in DevToys
    IconGlyph = '\uE670',                                                     // An icon that represents a pizza
    GroupName = PredefinedCommonToolGroupNames.Converters,                    // The group in which the tool will appear in the side bar.
    ResourceManagerAssemblyIdentifier = nameof(MyResourceAssemblyIdentifier), // The Resource Assembly Identifier to use
    ResourceManagerBaseName = "MyExtension.MyExtension",                      // The full name (including namespace) of the resource file containing our localized texts
    ShortDisplayTitleResourceName = nameof(MyExtension.ShortDisplayTitle),    // The name of the resource to use for the short display title
    LongDisplayTitleResourceName = nameof(MyExtension.LongDisplayTitle),
    DescriptionResourceName = nameof(MyExtension.Description),
    AccessibleNameResourceName = nameof(MyExtension.AccessibleName))]
internal sealed class MyExtensionGui : IGuiTool
{
    public UIToolView View =>new(
        Label()
            .Style(UILabelStyle.BodyStrong)
            .Text(MyExtension.HelloWorldLabel));

    public void OnDataReceived(string dataTypeName, object? parsedData)
    {
        throw new NotImplementedException();
    }
}
```
### Fluent API
`IGuiTool`を実装したクラスの `View`プロパティにコンポーネントを追加することで、UIのレイアウトを構築します。
コンポーネントはいわゆるFluent APIの形式で提供され、メソッドチェーンで組み立てることが可能です。

```cs
public UIToolView View
    => new UIToolView(
        Stack()
            .Vertical()
            .WithChildren(
                SingleLineTextInput()
                    .Title("Title")
                    .OnTextChanged(OnSingleLineTextInputTextChanged),
                SingleLineTextInput()
                    .Title("Read only")
                    .ReadOnly(),
                SingleLineTextInput()
                    .Title("No command bar")
                    .HideCommandBar(),
                SingleLineTextInput()
                    .Title("Copy command even when editable")
                    .CanCopyWhenEditable(),
                SingleLineTextInput()
                    .Title("Extra command")
                    .CommandBarExtraContent(
                        Button()
                            .Text("Click me!")
                            .AccentAppearance())));
```


### Settings Provider
各ツールの設定値は、DevToys側で管理する仕組みが用意されており、ツール側で保存処理などを実装する必要がありません。
「どのような設定値を保存するか」を定義するだけで簡単に値の保存と取得ができます。

https://devtoys.app/doc/articles/extension-development/guidelines/use-settings.html?tabs=windows

```cs
internal sealed class MyGuiTool : IGuiTool
{
    //bool型の設定値を定義
    private static readonly SettingDefinition<bool> mySetting
        = new(
            name: $"{nameof(mySetting)}",
            defaultValue: true);

    //SettingsProviderをインポート(DevToys側からSettingsProviderのインスタンスが注入される)
    [Import]
    private ISettingsProvider _settingsProvider = null;

    public UIToolView View
        => new UIToolView(
            Stack()
                .Vertical()
                .WithChildren(
                    Button()
                        .Text("Click me")
                        .OnClick(OnButtonClick)));

    private void OnButtonClick()
    {
        // 設定値の取得
        bool settingValue = _settingsProvider.GetSetting(mySetting);
        bool newValue = !settingValue;
        // 設定値の更新
        _settingsProvider.SetSetting(mySetting, newValue);
    }
```

また、設定値と連動したコンポーネントの作成もサポートされており、例えばトグルスイッチをクリックすると設定値のTrue/Falseが自動で切り替わるなどといったことが簡単にできます。

https://devtoys.app/doc/articles/extension-development/guidelines/UX/basic-input/setting.html

## 拡張機能のインストール
拡張機能はNuGetパッケージの形式で作成します。作成した拡張機能のパッケージは、DevToysの「拡張機能の管理」画面の「拡張機能をインストール」からパッケージファイル(.nupkg)を選択してインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/1b414a19-b193-99a3-8eaf-8508c09531be.png)

NuGet Gallelyで公開されている拡張機能は、一旦手元の環境にパッケージファイルをダウンロードしてから、同様の手順でインストールします。（タグ：devtoys-appで検索）

https://www.nuget.org/packages?q=Tags%3A%22devtoys-app%22

## 作ったもの
最後に、今回作った拡張機能を紹介します。

### Fluent Icon Finder

https://github.com/pierre3/DevToys.Extensions.FluentIconFinder

Microsoftがオープンソースで提供している[Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons)を検索するツールです。
検索したアイコンの名前やフォントのコードポイント（Glyph code）を取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/13d61865-0628-c48a-cf64-d67ad76714b7.png)

また、光栄なことにDevToysの[公式ドキュメント](https://devtoys.app/doc/articles/extension-development/guidelines/UX/create-custom-icon.html)にこのツールのリンクを載せていただきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/72ff7e33-0b91-ab8a-6ad4-6ec10d61e8a1.png)


### 作ろうとしたきっかけ
DevToys ではFluent UI System Iconsがフォントとして組み込まれていて、アイコンを表示するコンポーネントで利用できます。

アイコンは、以下の様にフォントのコードポイントをChar型で指定する必要があり、指定するにはコードポイントの数値（下の例では`E670`）が必要となります。

```cs
var icon = Icon("FluentSystemIcons", '\uE670').Size(48));
```

Fluent UI System Iconsのリポジトリには、利用できる[アイコンの一覧](https://github.com/microsoft/fluentui-system-icons/blob/main/icons_regular.md)はあるのですが、フォントのコードポイントが記載されておらず、どうやって取得すればよいのか分かりませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/c299ad1f-d7a1-16a3-3b55-39afffd1c8e4.png)

GitHubのソースコードを探すと、以下のような[JSONファイル](https://github.com/microsoft/fluentui-system-icons/blob/main/fonts/FluentSystemIcons-Regular.json)にコードポイントの定義がありました。

```json
{
  "ic_fluent_access_time_24_regular": 61697,
  "ic_fluent_accessibility_16_regular": 61698,
  "ic_fluent_accessibility_20_regular": 61699,
  "ic_fluent_accessibility_24_regular": 61700,
  "ic_fluent_accessibility_28_regular": 61701,
  "ic_fluent_add_12_regular": 61703,
  "ic_fluent_add_16_regular": 61704,
  "ic_fluent_add_20_regular": 61705,
  "ic_fluent_add_24_regular": 61706,
  ...
}
```

そのためアイコン１つを指定するにも、非常に面倒な手順が必要でした。
1. アイコン一覧から目的のアイコンを探す
2. JSONファイルでアイコン名を検索してコードポイントの数値を取得する
3. Iconコンポーネントに取得した数値を設定する

```cs
Icon("FluentSystemIcons", (char)61697);//10進数の場合はcharにキャストでOK
```

何とかならんものかなーと考えていたところ、ふと「これってDevToysの拡張機能のネタとして丁度良いのでは？」と思い至り、作ってみることにしました。

### 表示できないアイコンがある

この拡張機能を実装するにあたり、問題が１つありました。
Fluent System UI Iconの中には2Byteを超えるコードポイントの値を持つアイコンがあるのですが、DevToysが提供するIconコンポーネントではそれが表示できないという問題です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/6cb9903a-eb0e-7a71-7a95-0a3e5b18c189.png)

DevToysのIconコンポーネントでコードポイントを指定する`Glyph`プロパティがChar型となっていることが原因でした。C#のChar型は２Byteなので、それ以上の情報を渡すことができません。

```cs
public interface IUIIcon : IUIElement
{
    /// <summary>
    /// Gets the name of the font containing the icon.
    /// </summary>
    string FontName { get; }
    /// <summary>
    /// Gets the glyph corresponding to the icon in the <see cref="FontName"/>.
    /// </summary>
    char Glyph { get; }
    /// <summary>
    /// Gets the size of the icon.
    /// </summary>
    int Size { get; }
    //...
}
```

##### DevToysのコードを修正してみる
幸いDevToysはオープンソースですので、コードをフォークして自分で修正することができます。
上手くいけばDevToys本体にコントリビュートできるかも？という事で修正して[Pull Request](https://github.com/DevToys-app/DevToys/pull/1435)を出してみることに。

問題のアイコンを表示させること自体はそんなに難しくなく、`Glpyh`プロパティをInt型に拡張して
```cs
public interface IUIIcon : IUIElement
{
    //...
    
    /// <summary>
    /// Gets the glyph corresponding to the icon in the <see cref="FontName"/>.
    /// </summary>
    int Glyph { get; }
    
    //...
}
```

コンポーネントへ引き渡す際に`char.ConvertFromUtf32(Glyph)`で文字列に変換してあげることで解決しました。

```cs
<i id=@Id
   class="font-icon @(FinalCssClasses)"
   style="@StyleValue"
   @ref=Element
   @attributes="AdditionalAttributes"
   data-glyph='@(char.ConvertFromUtf32(Glyph))'
   aria-hidden="true"
   role="presentation">
</i>
```

ただ、そのまま修正するとAPIとして公開しているインターフェースを変える必要があり、既存の拡張機能が動かなくなってしまいます。そのため実際はもう少し込み入った実装になっています（詳細は[Pull Request](https://github.com/DevToys-app/DevToys/pull/1435)をご確認ください）。

こちらマージしていただき、[v2.0.6.0](https://github.com/DevToys-app/DevToys/releases/tag/v2.0.6.0)にてリリースされました！

## まとめ
### 拡張機能を実装してみた所感
DevToys拡張のAPIはシンプルで使いやすく、公式ドキュメントにリファレンスもあり、迷いなく実装できました。

UIのコンポーネントは、基本的なものは一通り揃っていてちょっとしたツールを作るには十分です。
ただ、ハンドルできるイベントがごく一部だったり、細かなレイアウトの調整などができなかったりと、物足りない部分が多いかもしれません。

個人的には、GUIツールを簡単に作れるプラットフォームとして、日々の業務のちょっとした手間を改善するツールなどを組み込んだりするのに良さそうかなと感じました。

### プレビュー版
DevToys2.0はまだPreviewが付いています。機能的にまだまだ不足している部分もありますが、必要な機能は自分で追加するなどして貢献できるチャンスかもしれません。

この記事を見て気になった方は、ぜひ一度試してみてください！