tmytのらくがき

個人の日記レベルです

Visual Studioのソリューションエクスプローラのフィルタ機能を拡張する

tl;dr

  • ソリューションエクスプローラの表示を、Gitの中身を比較して変更があったファイルでフィルタする拡張を作った
  • フィルタする拡張機能は、HierarchyTreeFilterProvider を実装してVisual Studio側に公開するといい感じで動くらしい
  • フィルタ機能をソリューションエクスプローラに統合するときは、<Parent guid="guidSHLMainMenu" id="IDG_VS_TOOLBAR_PROJWIN_FILTERS" /> にするといい

Gitの状態でソリューションエクスプローラをフィルタしたい

Visual Studioの標準状態にはGitのunstagedなファイルのみ表示するフィルタはあって、これを使いたいからできればコミットしたくない。みたいな話を聞いた。 f:id:tmyt:20200908023344p:plain

普通にコミットして、git diff --stats origin/master 的なコマンドをSource Treeとかで実行したら十分なのでは?っていう話をしたら、それVSのフィルタ機能に統合してほしい。って言われたので作ってみたよ。

ソリューションエクスプローラのフィルタ機能を拡張する

まず、ソリューションエクスプローラのフィルタ機能を実現するにはHierarchyTreeFilterProviderというクラスを実装すればいい。 大まかにはdocs.com に書いてるのを読めばだいたい動く。

docs.microsoft.com

まず、HierarchyTreeFilterProviderを実装して、SolutionTreeFilterProvider属性を付けて、protectedで定義されているHierarchyTreeFilterを実装すればOK。

HierarchyTreeFilterProviderはフィルタの実装をnewするためのファクトリクラスで、フィルタとしての実体はHierarchyTreeFilterVisual Studioがフィルタを実行したい気分になると、HierarchyTreeFilter.GetIncludedItemsAsyncが呼び出されるので、このメソッドのなかにフィルタロジックを書いていけばOK。

次に、フィルタを呼び出すボタンをVSCTファイルに書く。基本的にはボタンをツールバーに追加する時と同じで、親として設定するグループを適切に設定する。適切というのは、

<Parent guid="guidSHLMainMenu" id="IDG_VS_TOOLBAR_PROJWIN_FILTERS" />

こう書く。このIDG_VS_TOOLBAR_PROJWIN_FILTERSはどこから出てきたのはは気にしてはいけない。こう書け。と先のdocs.comにも書いてあるのでこう書く。気にしてはいけない。

Gitの状態にアクセスする

LibGit2Sharpを使ってアクセスした。Visual Studioが開いているソリューションは DTE.Solution.FullNameプロパティにアクセスすると得られるので、ここからディレクトリを上方向にたどった最寄りの.git をソリューションが参照すべきGitリポジトリとした。

このパスを、LibGit2Sharpで開いて、Diff.Compare<TreeChanges>を呼び出す。差分がある(Add, Delete, Modifiedなど)ファイルのみを返すのでこの一覧をソリューションエクスプローラに表示すべきファイルとして使った。 この一覧ができてしまえばあとは、_hierarchyCollectionProvider.GetFilteredHierarchyItemsAsync を呼び出して、コールバックにフィルタすべきかどうかを判定する項目が入っているので、CanonicalNameに含まれているパスをチェックすればOK。

LibGit2SharpをVS拡張から使う

LibGit2Sharpはlibgit2のC#バインディングで実行にはネイティブ実装が必要。で、本来はNuGetから自動でインストールされてbinに出力されるけれども、VSIXの場合はVSIXに明示的に含めるようにしないとだめ。なのでcsprojを編集して次を書き足しておく。

<ItemGroup>
  <PackageReference Include="LibGit2Sharp.NativeBinaries" GeneratePathProperty="true">
    <Version>2.0.306</Version>
  </PackageReference>
  <Content Include="$(PkgLibGit2Sharp_NativeBinaries)\runtimes\win-x64\native\git2-106a5f2.dll">
    <Link>lib\win32\x64\git2-106a5f2.dll</Link>
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    <IncludeInVSIX>true</IncludeInVSIX>
  </Content>
  <Content Include="$(PkgLibGit2Sharp_NativeBinaries)\runtimes\win-x64\native\git2-106a5f2.pdb">
    <Link>lib\win32\x64\git2-106a5f2.pdb</Link>
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    <IncludeInVSIX>true</IncludeInVSIX>
  </Content>
  <Content Include="$(PkgLibGit2Sharp_NativeBinaries)\runtimes\win-x86\native\git2-106a5f2.dll">
    <Link>lib\win32\x86\git2-106a5f2.dll</Link>
    <IncludeInVSIX>true</IncludeInVSIX>
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
  <Content Include="$(PkgLibGit2Sharp_NativeBinaries)\runtimes\win-x86\native\git2-106a5f2.pdb">
    <Link>lib\win32\x86\git2-106a5f2.pdb</Link>
    <IncludeInVSIX>true</IncludeInVSIX>
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
  <Content Include="Resources\SolutionFilter.png" />
</ItemGroup>

これで、NuGetからLibGit2Sharp.NativeBinariesがインストールされて、x86, x64 Windows向けバイナリがVSIXに含まれるようになる。

ベースブランチを選択する

どこかをDiffの基準点にする必要がある。今回は、WPFの非表示のWindowとContextMenuを動的に生成して、ボタンを押したときにポップアップメニューが表示されるようにした。

f:id:tmyt:20200908025438p:plain

ソリューションエクスプローラのこのツールバーにボタンを追加する方法は探しても見つからないけれども、VSCTの親設定にこう書けば追加できる。

<Parent guid="guidSharedMenuGroup" id="IDG_VS_PROJ_TOOLBAR2" />

これをどうやって探すかはdocs.comに詳しくはないけど書いてある。

docs.microsoft.com

SharedCmdPlace.vsctこのファイルに定義されているのが定義済みのコマンドバーなどなどの場所で、このファイルはこのあたりに入っていた。 C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VSSDK\VisualStudioIntegration\Common\Inc

この中から、ソリューションエクスプローラツールバーを探すと、IDG_VS_PROJ_TOOLBAR2がみつかる。

おしまい

成果物はGitHubVisual Studio Marketplaceに公開してあります。ローカライズもうまくいかなくてだいぶ悩んだけどそれはまた別の機会に…

github.com

marketplace.visualstudio.com