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

wslのpathにある実行ファイルをwindowsから実行できるやつを作った

WSLって歴史的経緯で今も bash.exe で起動ができるけど、 bash.exe で起動したときは chsh で設定されたシェルではなく常にbashが起動してしまう。たぶん内部的に /usr/bin/bash を実行してるんだとおもう*1

wsl を呼べば chsh で設定されたシェルが起動はするのだけどせっかくなので zsh.exe を起動するとデフォルトのWSL環境でzshが実行されるようなやつを作ってみました。

github.com

やっていることはarg[0]からファイル名を取り出して拡張子を除去、引数部分は無加工のまま受け取ってそれらをくっつけてWSLプロセスを起動。 実行ファイルの名前を変えると*2起動するWSLプロセスが変わる仕組みです。busyboxみたいな挙動ですね。

せっかくなのでWSLでプロセスを起動したりすることについて残しておきます。

WSL内でのプロセスの実行

WSL内でプロセスを実行するには大昔は非公開APIをCOM経由で呼ばないといけなかったのだけど、いまではちゃんと公開APIが用意されているのでそれを呼ぶだけ。

プロセスの実行方法は2種類用意されていて

  • 標準入出力をパイプで渡してhProcessを返すパターン
  • 現在実行中のプロセスの標準入出力を使ってプロセスが終了するまでAPIが返ってこないパターン

コンソールを持たないプロセスの場合とかは前者、コンソールでWSLプロセスが終了するまで制御を返さなくていいなら後者でいいと思う。

今回の場合は単純にWSL内でプロセスが実行できて終了すれば自分自身も終了するので後者。

docs.microsoft.com

このAPIに、実行したいディストリビューションの名前、実行したいパスなどを指定するだけでWSLでプロセスが実行できるとても簡単。

ひとつ問題があって、ドキュメントには wslapi.lib をリンクせよ。と書いているのだけどこれがSDKになぜか含まれていない。なぜ含まれていないのかは謎だけど、実際なぜか含まれていない。 しかたないので、wslapi.dll をダイナミックリンクして使いましょう。

デフォルトディストリビューションの解決

ドキュメントを見てもデフォルト設定のディストリビューションがどれなのかを取得することはどうやらできなさそうです。これはレジストリを見ると解決できます。

WSLに関する設定は HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss 配下に収まっています。ここの、DefaultDistributionに書き込まれている名前でサブキーをたどると そこにデフォルト設定になっているディストリビューションの情報が埋まっています。

WslLaunch*ディストリビューションの名前を必要としているので、サブキーから DistributionName の値を読むことでデフォルト値が解決できます。

おしまい

こんな感じでWSLのプロセスがWindows側から起動できるそうです。

zshが起動したいためだけに作ったけれども、python.exeとかgrep.exeとか用意したらなかなか楽しくなってきた。

*1:調べてないです

*2:実行ファイルをコピーしてリネームとか、ハードリンクするといいと思います。シンボリックリンクだとExplorerがリンク先の実行ファイルを起動してしまってうまく動かない。

UWPでタスクバーのアイコンをLight/Darkで切り替える

tl;dr

  • 通常は theme-light
  • シェル用のリソース(altform-unplatedなやつ)はaltform-lightunplated

Lightテーマのシェル向けのリソース定義

Windows 10 1903でタスクバーとかがLightテーマにできるようになったのにあわせて、UWPのリソースもライトテーマ向けのアイコンを含められるようになっています。

そもそもタスクバーとかで利用されるアイコンは altform-unplated だったけれどもLightテーマだと真っ白けでなにもわからんくなってしまうのでどうにかしたというやつ。

docs.microsoft.com

ライトテーマ固有のアセットを提供する シェルライトテーマ用に調整されたリソースを提供するアプリでは、新しい代替フォームリソース修飾子である altform-lightunplatedを使用できます。 この修飾子は、既存の altform-プレートなし修飾子を反映します。

機械翻訳されて若干あやしめだけど、ファイル名の altform-unplatedaltform-lightunplated に変更することでLightテーマ用のリソースとして扱えます。

ただ、最近は白一色/黒一色アイコンやめる風潮なので近いうちにいらなくなるかもしれない。

WSL2のVHDを移動する

TL;DR

  • export/importすれば移動できます
CMD> wsl --export Debian debian.tar
CMD> wsl --unregister Debian
CMD> wsl --import Debian D:\wsl\Debian\ debian.tar
  • import後のデフォルトアカウントがrootになる
    • ファイル名を指定して実行でコマンドを実行した
debian config --default-user {your_name}

WSL2のVHDを移動したい

WSL2はWSL1と違ってext4フォーマットのrootfsをVHD形式でWindows側に保持してます。いろいろ使ってると案外大きくなっていって気が付いたら40GBも使ってました。

デフォルトでは %LocalAppData\Packages\{package_name} の中にVHDが作成されてまぁまぁ邪魔なのでほかのディスクへ移動できないかぐぐったらGitHubにその通りのIssueがあったので同じことをやります。 IssueはUbuntuですが、手元はWSL2はDebianで使ってるのでSnippetはDebianに変わっています。

github.com

まず移動。

> wsl --export Debian debian.tar
> wsl --unregister Debian
> wsl --import Debian D:\wsl\Debian\ debian.tar

export時点で40GBのtarが一時的にできて、unregisterでVHDが削除、その後importで40GBのtarから40GBのVHDが目的の場所にできてました。

export/import後に起動してみるとデフォルトアカウントがrootになっていたのでこれを修正。 デフォルトアカウントを変更するには debian config --default-user {your_name} を実行したらいいらしい。

コマンドプロンプトからだとなぜか動かなかったので、ファイル名を指定して実行からコマンドを実行してデフォルトユーザを変更。

うまくいきました。

ASP.NET CoreのTagHelperでいろいろするメモ

TagHelper

TagHelperを継承したクラスで、ProcessAsyncをoverrideしていろいろすると、出力をいろいろできる。

いろいろする

// ターゲットにするタグ名をここに付ける
[HtmlTargetElement("my:Example")] 
public class ExampleTagHelper : TagHelper
{
  // ViewContextオブジェクトを格納してほしい時に書く。
  // これがあると、TagHelperの内側からHttpContextとかにアクセスし放題になる
  [ViewContext]
  // 属性をBindしたくないときに付ける
  [HtmlAttributeNotBound]
  public ViewContext ViewContext { get; set; }

  // プロパティにバインドしたい属性を書く
  [HtmlAttributeName("hello")] 
  public string Hello { get; set; } = "";

  // これをoverrideしていろいろする
  public override Task ProcessAsync(
    // Bindした属性のリストとかはこっち
    TagHelperContext context,
    // Bindしてない属性のリストとか、出力するタグの設定とかはこっち
    TagHelperOutput output)
  {
    // 子要素のHTML-string はこれで読める
    var content = (await output.GetChildContentAsync()).GetContent();
    // タグを my:Example から div に変える
    output.TagName = "div";
    // 自己終了タグとして使いたいTagHelperなのに出力されるHTMLが自己終了だと都合が悪い時などにTagModeを変える
    // output.TagMode = TagMode.StartTagAndEndTag;
    // 属性を好きに付け替えたりできる
    output.Attributes.SetAttribute("style", "color: red");
    // 中身も好き勝手編集できる
    output.PreElement.SetHtmlContent("[1]");
    output.PreContent.SetHtmlContent("[2]");
    output.Content.SetHtmlContent($"[3] {Hello}");
    output.PostContent.SetHtmlContent("[4]");
    output.PostElement.SetHtmlContent("[5]");
    return Task.CompletedTask;
  }
}

このTagHelperをRazorからこんな感じで呼ぶ

<my:Example hello="world" />

そうすると、こう出力される。

[1]
<div>
[2]
[3] world
[4]
</div>
[5]

便利。

Windows 10 2004のエクスペリエンスっていう項目が気になったので探した

Windows 10 2004 May 2020 Updateにするとシステムのバージョン情報に"Windows Feature Experience Pack"というのが表示されるようになりました。らしい。この環境はInsider Fast Ringなので若干バージョンが違うけれども、2004でも同じことができます。

f:id:tmyt:20200620205541p:plain

Feature on Demand版のドキュメントを見てみると

This Feature on Demand package includes features critical to Windows functionality. Do not remove this package.

  • Capability Name: Windows.Client.ShellComponents~~~~0.0.1.0
  • Sample package name: Microsoft-Windows-UserExperience-Desktop-Package~31bf3856ad364e35~amd64~~.cab

と、いうことらしい…中身が気になったので探してみます。

おそらくこれはAppX形式だろう。ということで、PowerShellでパッケージを探してたのがこれ。

PS> Get-AppxPackage | Where-Object { $_.Version -eq "120.13701.0.0" }
Name              : MicrosoftWindows.Client.CBS
Publisher         : CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US
Architecture      : X64
ResourceId        :
Version           : 120.13701.0.0
PackageFullName   : MicrosoftWindows.Client.CBS_120.13701.0.0_x64__cw5n1h2txyewy
InstallLocation   : C:\Windows\SystemApps\MicrosoftWindows.Client.CBS_cw5n1h2txyewy
IsFramework       : False
PackageFamilyName : MicrosoftWindows.Client.CBS_cw5n1h2txyewy
PublisherId       : cw5n1h2txyewy
IsResourcePackage : False
IsBundle          : False
IsDevelopmentMode : False
NonRemovable      : True
Dependencies      : {Microsoft.VCLibs.140.00_14.0.27810.0_x64__8wekyb3d8bbwe}
IsPartiallyStaged : False
SignatureKind     : System
Status            : Ok

このパッケージは C:\Windows\SystemApps に入ってるらしいので、管理者権限のコマンドラインから様子を見てみることにします。

PS> dir

    ディレクトリ: C:\Windows\SystemApps\MicrosoftWindows.Client.CBS_cw5n1h2txyewy

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2020/06/14      7:20                AppxMetadata
d-----        2020/06/14      7:20                Assets
d-----        2020/06/14      7:20                InputApp
d-----        2020/06/14      7:19                pris
d-----        2020/06/14      7:20                ScreenClipping
d-----        2020/06/14      7:20                WindowsInternal.ComposableShell.Experiences.SuggestionUIUndocked
-a----        2020/06/13      7:19         877169 AppxBlockMap.xml
-a----        2020/06/13      7:19          58385 AppxManifest.xml
..省略..

AppXらしいデータが入ってました。中身が気になるのでAppxManifest.xmlを眺めてみます。

> Get-Content .\AppxManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" xmlns:uap6="http://schemas.microsoft.com/appx/manifest/uap/windows10/6" xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10" xmlns:wincap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/windowscapabilities" IgnorableNamespaces="mp uap uap3 uap4 uap6 uap10 wincap">
        <Identity Name="MicrosoftWindows.Client.CBS" Publisher="CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" Version="120.13701.0.0" ProcessorArchitecture="x64"/>
        <mp:PhoneIdentity PhoneProductId="3898fac3-1f84-4040-83ce-ef30739c0a64" PhonePublisherId="6145b212-a274-4517-8abe-1e015f21ff03"/>
        <Properties>
                <DisplayName>Windows Feature Experience Pack</DisplayName>
                <PublisherDisplayName>Microsoft Windows</PublisherDisplayName>
                <Logo>Assets\StoreLogo.png</Logo>
        </Properties>
..省略..

こういうのがだらだら続くのだけれども、めっちゃ長いのでピックアップして。

どうやらInputMethod的ななにかだそうです。

<Application Id="InputApp" Executable="TextInputHost.exe" EntryPoint="WindowsInternal.ComposableShell.Experiences.TextInputUndocked.InputApp.App">
..省略..
  <Extensions>
    <!-- VS's manifest validation won't accept an empty Extensions section but it is required for
  fragment merging, so add a dummy protocol entry -->
    <uap:Extension Category="windows.protocol">
      <uap:Protocol Name="ms-inputapp" DesiredView="useMinimum">
        <uap:DisplayName>Input App</uap:DisplayName>
      </uap:Protocol>
    </uap:Extension>
    <uap3:Extension Category="windows.appExtension">
      <uap3:AppExtension Name="com.microsoft.windows.input.app" Id="InputApp" PublicFolder="Public" DisplayName="Input Experience">
      </uap3:AppExtension>
    </uap3:Extension>
  </Extensions>
</Application>

Windpws+Shift+Sで出てくるあれの実体がここに入っているそうです。

<Application Id="ScreenClipping" Executable="ScreenClippingHost.exe" EntryPoint="ScreenClippingHost.App">
..省略..
  <Extensions>
    <uap:Extension Category="windows.protocol">
      <uap:Protocol Name="ms-screenclip">
        <uap:DisplayName>ms-screenclip</uap:DisplayName>
      </uap:Protocol>
    </uap:Extension>
    <uap3:Extension Category="windows.appExtension">
      <uap3:AppExtension Name="com.microsoft.windows.app.screenclip" Id="ScreenClippingApp" PublicFolder="Public" DisplayName="Screen Snipping Experience"/>
    </uap3:Extension>
    <uap3:Extension Category="windows.appExtensionHost">
      <uap3:AppExtensionHost>
        <uap3:Name>com.microsoft.windows.protocoloverride</uap3:Name>
      </uap3:AppExtensionHost>
    </uap3:Extension>
  </Extensions>
</Application>

今のところこれくらいしか中身はないのだけれども、EntryPointの名前とかパッケージの名前とかを見るとそのうちいろいろな実体がここに移されてくるんだろうなぁ…という感じでした。