WPFでGitのDiffっぽい差分表示をするDiffPlexライブラリ。～あるいは、あるジェダイの変遷

# 前説

<FONT COLOR=skyblue>$\style{background-color:black;}{遠い昔、はるかかなたの銀河系で……}$</FONT>
<FONT COLOR=Yellow>$\style{background-color:black;}{　　　　　　　　　　　　　　　　　　　　}$
$\style{background-color:black;}{Ｑｉｉｔａには混乱が渦巻いていた。非公開にされる記事の基準の是非で意見が割れたのだ。}$
<FONT COLOR=Yellow>$\style{background-color:black;}{数千のユーザーがＱｉｉｔａからの離脱を表明したのだ。}$
$\style{background-color:black;}{この非常事態にコメント欄は果てしない討議をただ繰り返すばかり。}$
$\style{background-color:black;}{戦争だ！それはともかく、「フォースにバランスをもたらす者」と言われた美少年アナキン=スカイウォーカーであったが、}$
$\style{background-color:black;}{なんやかんやあって、年齢も重ね、変なニックネームも付き、住所不定になっていた。}$
$\style{background-color:black;}{そこで少年時代（Episode1）とおっさん時代（Episode4）での変化を差分で表示してみよう。}$
<FONT COLOR=Yellow>$\style{background-color:black;}{人類を救い銀河に自由を取り戻すために....}$</FONT>

# 概要

この記事ではWPFで2つの文字列の差分を表示できるDiffPlexというライブラリを紹介します。GitなどのDiff表示的なやつです。
![スクリーンショット 2020-04-15 22.34.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/cc666540-8f98-fc96-dbce-56e7de49241c.png)
こんな感じに左右で2つのテキストを比較できます。
変なニックネームがついたので3行目に`NickName`プロパティが追加され、年をとったので5行目の`Age`プロパティが変更され、住所不定になったので6行目の`Place`プロパティが削除されています。

# 方法
## 左右分割表示
WPFアプリを作成して、nugetで、[DiffPlex.Wpf](https://www.nuget.org/packages/DiffPlex.Wpf/1.1.1?_src=template)を取得
MainWindow.xamlに`diffplex:DiffViewer`を追加して、比較したいテキストを追加します。

```xml:MainWindow.xaml（部分）
<Window
...
   xmlns:diffplex="clr-namespace:DiffPlex.Wpf.Controls;assembly=DiffPlex.Wpf"
...>
      <diffplex:DiffViewer
         NewText="{Binding NewText}"
         OldText="{Binding OldText}"/>    
</Window>
```

これだけです。
あとはオプションで差分のタイトル部分に文字指定をしたかったら`OldTextHeader`・`NewTextHeader`を使用してください。

## インライン表示

また、左右分割ではなく、インライン表示もできます。
![スクリーンショット 2020-04-15 22.34.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/e3491dc4-6a6e-1911-4067-b715379505d4.png)

~~こちらはなぜかXAMLからはできないので、コードビハインドで変更します。~~
プルリク送ったら採用してくれて、Ver1.1.2からXAMLで指定できるようになりました。

```xml:MainWindow.xaml（部分）
<Window
...
   xmlns:diffplex="clr-namespace:DiffPlex.Wpf.Controls;assembly=DiffPlex.Wpf"
...>
      <diffplex:DiffViewer
         IsSideBySide ="false"
         NewText="{Binding NewText}"
         OldText="{Binding OldText}"/>    
</Window>
```

## カスタム表示

さらにもっとカスタムして、Viewを独自形式にすることもできます。
![スクリーンショット 2020-04-15 22.34.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/61716cf0-0fb9-2249-82c5-2db4b201424d.png)

変更内容で背景色と行頭絵文字を変更しています。
今回はRichTextBoxで表示することにします。

### View

```xml:MainWindow.xaml（部分）
<Window
...
   xmlns:diffplex="clr-namespace:DiffPlex.Wpf.Controls;assembly=DiffPlex.Wpf"
...>
   <Grid>
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="*" />
         <ColumnDefinition Width="Auto" />
         <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <RichTextBox Grid.Column="0" local:RichTextBoxHelper.Document="{Binding DiffModel.OldText, Converter={StaticResource RichTextVmToFlowDocumentConverter}}" />
      <TextBlock
         Grid.Column="1" VerticalAlignment="Center"
         FontSize="24" Text="👉" />
      <RichTextBox Grid.Column="2" local:RichTextBoxHelper.Document="{Binding DiffModel.NewText, Converter={StaticResource RichTextVmToFlowDocumentConverter}}" />
   </Grid>  
</Window>
```

描画内容がほとんどViewModelとConverterに書いてあるので、Viewはシンプルです。
`RichTextBoxHelper.Document`添付プロパティについては下記リンクを参照ください。
今回は下記リンク内の**方法3 添付プロパティとConverterで柔軟にBindingする**を使用します。

[WPFのRichTextBoxにBindingする](https://qiita.com/soi/items/ac970626af21aa5a362f)

### ViewModel

RichTextBoxの中身(`Document`）にViewModelで相当するのが、`DiffPaneModel`クラスです。

```csharp:MainWindowViewModel.cs(部分)
class MainWindowViewModel : INotifyPropertyChanged
    {   
...   
        private SideBySideDiffModel _DiffModel;
        public SideBySideDiffModel DiffModel
        {
            get => _DiffModel;
            set
            {
                _DiffModel = value;
                RaisePropertyChanged();
            }
        }

        public MainWindowViewModel()
        {
            var builder = new SideBySideDiffBuilder(new Differ());
            DiffModel = builder.BuildDiffModel(OldText, NewText);
        }
   }
```

`SideBySideDiffModel`はDiffPlexライブラリ内のクラスです。

`new SideBySideDiffBuilder(new Differ())`で差分表示データを作るためのビルダーが生成されます。
そこに`builder.BuildDiffModel(古文字列, 新文字列)`で比較したい文字列を入力して、差分表示データが生成します。

`SideBySideDiffModel`クラスの`DiffPaneModel`型の`OldText`と`NewText`のプロパティが差分比較対象の文字列に相当します。
`DiffPaneModel`の`List<DiffPiece>`型の`Lines`プロパティが文字列内の各行に相当します。
`DiffPiece`の`ChangeType`型の`Type`プロパティがその行の変更状態を表しています。

|   ChangeType |    内容    |
|---------------:|:----------------:|
|   Unchanged |   変更なし    |
|   Deleted |   行削除    |
|   Inserted|   行追加    |
|   Imaginary |  追加される前・削除された後の行    |
|   Modified |   行内の文字列変更    |

さらに`DiffPiece`の`List<DiffPiece>`型の`SubPieces`プロパティが各単語に相当します。
例えば、単語の区切りに`|`を追加してみるとこのようになります。
`    public int Age { get; set; } = 9;`
`|    |public| |int| |Age| { |get|; |set|; } |=| |9|;`

そして単語ごとにも`Type`プロパティがあります。行の`ChangeType`が`Modified`の場合は変更部分の単語だけ`ChangeType`が`Unchanged`以外になっています。
上の例では最後から2つ目の`9`の`DiffPiece`は`ChangeType`が`Deleted`になっています。

### Converter

このViewModelをConverterでRichTextBoxの中身(`Document`）にBindingできるよう、`FlowDocument`型に変換しています。

```RichTextVmToFlowDocumentConverter.cs
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is DiffPaneModel diffVM))
            return Binding.DoNothing;

        var paragraph = new Paragraph();
        foreach (var lineVM in diffVM.Lines)
        {
            List<Run> lineView = ConvertLinveVmToRun(lineVM);

            paragraph.Inlines.AddRange(lineView);

            //改行を追加
            paragraph.Inlines.Add(new LineBreak());
        }
        return new FlowDocument(paragraph);
    }

    private static List<Run> ConvertLinveVmToRun(DiffPiece lineVM)
    {
        //差分タイプによって、行頭の文字列内容と背景色を決定
        var (color, preFix) = lineVM.Type switch
        {
            ChangeType.Deleted => (Colors.Pink, "💣| "),
            ChangeType.Inserted => (Colors.GreenYellow, "➕| "),
            ChangeType.Imaginary => (Colors.SkyBlue, "📌| "),
            ChangeType.Modified => (Colors.Yellow, "✏| "),
            _ => (Colors.Transparent, "🔏| "),
        };

        //見やすいように少し半透明にしておく
        color.A = 0xC0;
        var baseColorBrush = new SolidColorBrush(color);
        var modifiedPieceBrush = new SolidColorBrush(Colors.Orange);

        //ChangeType.Modified以外は行全体で同じ書式
        if (lineVM.Type != ChangeType.Modified)
        {
            var lineView = new Run()
            {
                Text = preFix + lineVM.Text,
                Background = baseColorBrush,

            };
            return new List<Run> { lineView };
        }

        //ChangeType.Modifiedだったら変更された部分だけハイライトしたいのでSubPieceからいろいろやる
        var prefixRun = new Run()
        {
            Text = preFix,
            Background = baseColorBrush,
        };
        var runs = new List<Run> { prefixRun };

        foreach (var piece in lineVM.SubPieces)
        {
            runs.Add(new Run
            {
                Text = piece.Text,
                Background = piece.Type == ChangeType.Unchanged
                    ? baseColorBrush
                    : modifiedPieceBrush,
            });
        }
        return runs;
    }
```

FlowDocumentとDiffPaneModelの対応は以下のようにしました。

| DiffPaneModel                                              | FlowDocument              | 内容       |
| ---------------------------------------------------------- | ------------------------- | ---------- |
| DiffPaneModel      | Paragraph               | 文字列全体 |
| DiffPiece DiffPaneModel.Lines[n] | Run + LineBreak     | 1行  (ChangeType.Modified以外の場合)        |
| DiffPiece DiffPaneModel.Lines[n]   | List<Run> + LineBreak | 1行 (ChangeType.Modifiedの場合)       |
| DiffPiece DiffPiece.SubPieces[n]   | Run                 | 1単語 (ChangeType.Modifiedの場合)      |

このように、VMからView(`Run`）に変換します。
ある行が`ChangeType.Modified`以外の場合は行全体で1個の`Run`になります。
`ChangeType.Modified`の場合は、変更する単語部分だけハイライトしたいので、1行を複数の`Run`で構成します。
変換する際に`ChangeType`に応じて、文字の背景色と行頭に絵文字を追加しています。

これにより、ViewModelのDiffPaneModelからViewのRichTextBoxに変換してBindingができました。

## 全体コード
以下に置いておきます。
https://github.com/soi013/DiffPlexDemo

# 参考
https://github.com/mmanela/diffplex
https://github.com/halllo/WpfDiff

# 環境
VisualStudio 2019
.NET Core 3.1
C# 8
DiffPlex 1.7.0
DiffPlex.Wpf 1.3.1

# あとがき
Qiita のHTMLタグで遊んでみたかった。