tmytのらくがき

個人の日記レベルです

VSCTをローカライズする

Visual Studio拡張でボタンとか追加する時はVSCTというXMLで配置先とか、ラベルとかを定義して、それがコンパイラでバイナリになって、リソースに押し込まれます。 で、それをローカライズするにはどうすればいいか、というとdocs.com のここに書いています。

docs.microsoft.com

この通りにいくらやってもうまくいかなくて、うまくいくcsprojができたので書き残しておきます。

AssemblyInfo.cs を編集

csprojを編集する前にまずAssemblyInfo.csにひとつ追加。これでフォールバックロケールの検索先が変わるそうです。

// これを追加する。この場合は en-us がデフォルト言語。それ以外をデフォルトにしたいときは、そのロケール文字列を指定するとOK
[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]

これを書いておかないと、デフォルトのフォールバックリソースにVSCTのコンパイル結果が含まれていないので、フォールバックリソースが使われたVisual StudioでVSCTが読み込まれません。

VSCTをローカライズする

VSCTのXMLローカライズ済み文字列定義を参照するみたいな賢いことはできないので、全ロケール分コピーします。

例えば、Menus.vsctというファイルがある状態で、en-USとja-JPに対応したい場合はMenus.en-US.vsct、Menus.ja-JP.vsct をコピーして作ります。それぞれ中身はよさげにローカライズします。

f:id:tmyt:20200908063511p:plain

コピー元のMenus.vsctは消しても消さなくても大丈夫。もし、カスタムツールに"VsctGenerator"を指定している場合は残してもいいし、使ってない場合は削除でOK。 カスタムツールを使っている場合でも、ビルドアクションは"なし"にしてOK。

Resxをローカライズする

Resxもローカライズします。しないとうまく動きません。Resxのローカライズはいつもと同じ感じで、Strings.resxをStrings.en-US.resxとStrings.ja-JP.resxにコピーしてそれぞれローカライズみたいな感じでOK*1

ただし、フォールバック言語(ここではen-US)も個別言語としてつくらないとだめ。ここでフォールバック言語用のresxは消してはいけない。消すと動かない。

f:id:tmyt:20200908063645p:plain

この時、Resxがプロジェクトにひとつもない場合は空で大丈夫なので追加しておく。追加しておかないとうまく動かない

csprojを調整する

ここでcsprojをアンロードして右クリック→編集する。そして、こんな感じにする。

<!-- vsct -->
<ItemGroup>
  <!-- PackageGuidsとかを自動生成するのであればこれが必要 -->
  <None Include="ExpandAllPackage.vsct">
    <Generator>VsctGenerator</Generator>
    <LastGenOutput>ExpandAllPackage1.cs</LastGenOutput>
  </None>
  <!-- 各言語版をコピーして作る -->
  <VSCTCompile Include="ExpandAllPackage.ja-JP.vsct">
    <!-- ResourceNameはすべての言語でMenus.ctmenuにする -->
    <ResourceName>Menus.ctmenu</ResourceName>
  </VSCTCompile>
  <!-- ほかの言語と同じ。対応する言語分編集する。 -->
  <VSCTCompile Include="ExpandAllPackage.en-US.vsct">
    <ResourceName>Menus.ctmenu</ResourceName>
  </VSCTCompile>
</ItemGroup>
<ItemGroup>
  <Content Include="Resources\ExpandAll.png" />
</ItemGroup>
<!-- resx -->
<ItemGroup>
  <!-- 必須。ないとvsctがロードされない。リソースをコードから参照するときもこのリソースからバインディングが生成されるので消してはだめ -->
  <EmbeddedResource Include="Resources\Strings.resx">
    <Generator>ResXFileCodeGenerator</Generator>
    <LastGenOutput>Strings.Designer.cs</LastGenOutput>
    <!-- なんか適当に設定する。resxをresourcesに書き換えたものでOK。 -->
    <LogicalName>Strings.resources</LogicalName>
    <!-- ここには MergeWithCTOを書かない。書くとビルドできない。 -->
  </EmbeddedResource>
  <!-- 普通にローカライズするときと同じ。全言語作る。 -->
  <EmbeddedResource Include="Resources\Strings.en-US.resx">
    <!-- これをつけるとVSCTのコンパイル結果がこのresxとマージされる。ローカライズ済みリソースですべて必須。 -->
    <MergeWithCTO>true</MergeWithCTO>
    <!-- おなじ。resxをresourcesに書き換えたものでOK -->
    <LogicalName>Strings.en-US.resources</LogicalName>
  </EmbeddedResource>
  <!-- ほかの言語と同じ。対応する言語分編集する。 -->
  <EmbeddedResource Include="Resources\Strings.ja-JP.resx">
    <MergeWithCTO>true</MergeWithCTO>
    <LogicalName>Strings.ja-JP.resources</LogicalName>
  </EmbeddedResource>
</ItemGroup>

MergeWithCTOとLogicalNameは、resxの組のいずれかでOK。*2

編集したら、保存してプロジェクトを再読み込みする。あとはビルドすればおそらく複数言語に対応したVisual Studio拡張が出来上がっているはず。

ちなみに、VSCTが不要な場合はResxだけなのでこんな面倒なことをしなくてもローカライズできます*3

Visual Studioの言語を切り替えると、正しくローカライズされているように見える。

f:id:tmyt:20200908064008p:plain

*1:docs.comには、Strings.resxをStrings.en-US.resxにリネームするって書いてるけどここがたぶん間違い。

*2:たとえば、Strings.resxとBitmaps.resxがある場合は、Strings.resx(ja-JP, en-US)か、Bitmaps.resx(ja-JP, en-US)のいずれかでOK。ここではResxが1グループしかないので、こういうcsprojになっている。

*3:AssemblyInfo.csの編集もいらないしcsprojの編集もいらないし、ロケールのついてないresxがフォールバック先として読み込みもできる。

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} を実行したらいいらしい。

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

うまくいきました。

Chromeの <input type="time"> の時計アイコンを消す

Stackoverflowで見つけました。

stackoverflow.com

最近のChrome<input type"time"> を使うと右端に時計アイコンが出ます。これを消したい。

input[type="time"]::-webkit-calendar-picker-indicator {
    background: none;
}

こうすると消えるみたい。大感謝…

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]

便利。