yamamoWorks

.NET技術を中心に気まぐれに更新していきます

Visual Studioスタートページの「最近使用したファイル」を自動で整理

昔から気になっていたのです、Visual Studioのスタートページにある「最近使用したファイル」にゴミが残ることを。

コードの動作確認などで一時的なプロジェクトを作っては消すなんてことは皆さん多々あると思うのですが、「最近使用したファイル」は既に存在しないプロジェクトも表示されてしまうんですよね。

そこで、勉強も兼ねて「最近使用したファイル」から存在しないエントリーを削除するVS拡張を作って公開してみました。

visualstudiogallery.msdn.microsoft.com

Visual Studio 2015の拡張機能と更新プログラムからもインストール可能です。 f:id:yamamoWorks:20161229220403p:plain

これをインストールしておくと「最近使用したファイル」から存在しないエントリーを自動で削除してくれます。 f:id:yamamoWorks:20161229220645p:plain

解説

折角ですので仕組みも解説しておきます。 まず「最近使用したファイル」はレジストリに記録されています。

HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\14.0\MRUItems\{a9c4a31f-f9cb-47a9-abc0-49ce82d0b3ac}\Items

VS拡張でここに記録されているソリューションファイル/プロジェクトファイルの存在確認をしてレジストリを書き換えればいいので朝飯前ですね。

まずExtensibilityにあるVSIX Projectプロジェクトを作成します。 f:id:yamamoWorks:20161229222143p:plain

次にExtensibilityにあるVisual Studio Packageをプロジェクトに追加します。 f:id:yamamoWorks:20161229222557p:plain

するとPackageクラスを継承したクラスが自動生成されているのでInitializeメソッドをオーバライドで実装します。

Registry

PackageクラスにはUserRegistryRootプロパティがあり「HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\14.0」を表すRegistryKeyを返してくれるので便利です。後は目的のサブキーを開いて書き換えるだけです。

protected override void Initialize()
{
    base.Initialize();

    var regKey = UserRegistryRoot.OpenSubKey("MRUItems")?
        .OpenSubKey("{a9c4a31f-f9cb-47a9-abc0-49ce82d0b3ac}")?
        .OpenSubKey("Items", true);

    if (regKey == null)
        return;

    var mruItems = regKey.GetValueNames()
        .Select(name => (string)regKey.GetValue(name))
        .Where(value => File.Exists(value.Split('|').First()))
        .ToArray();

    foreach (var name in regKey.GetValueNames())
    {
        regKey.DeleteValue(name);
    }

    for (var i = 0; i < mruItems.Length; i++)
    {
        regKey.SetValue(i.ToString(), mruItems[i]);
    }

    regKey.Close();
}

デバック実行すると別のVisual Studioが起動して動作確認が可能です。 ちなみに、ここで起動してくるVisual Studioが参照するレジストリは「... \VisualStudio\14.0Exp\ ...」となっており開発環境と混在しないようになっています。

で、上記コードで動かしてみると・・・少し残念な挙動となります。

実は「最近使用したファイル」に表示する内容はVS拡張のInitializeメソッドが呼ばれる前に既に読み込まれており 、Initializeメソッドでレジストリを変更してもその時の起動では反映されないのです。 もちろん、次に起動した際には反映された状態になります。

この挙動では納得がいきません (・へ・)

そこで「最近使用したファイル」が表示される仕組みをILSpyで探り、何とか起動時に反映されるように実現できました。

ProjectMruList

「最近使用したファイル」の処理で中心となるのが Microsoft.VisualStudio.Shell.UI.Internal.dll にある Microsoft.VisualStudio.PlatformUI.ProjectMruList というInternalのクラスです。
Internalです・・・黒魔術決定。

ProjectMruListのインスタンスはDataSourceFactoryから{9099ad98-3136-4aca-a9ac-7eeeaee51dca}のIDで取り出せる事が解ったので、下記のコードで実現できました。

protected override void Initialize()
{
    base.Initialize();

    var dataSourceFactory = GetService(typeof(SVsDataSourceFactory)) as IVsDataSourceFactory;
    if (dataSourceFactory == null)
        return;

    IVsUIDataSource projectMruList;
    dataSourceFactory.GetDataSource(Guid.Parse("9099ad98-3136-4aca-a9ac-7eeeaee51dca"), 1, out projectMruList);

    var projectMruListType = Type.GetType("Microsoft.VisualStudio.PlatformUI.ProjectMruList, Microsoft.VisualStudio.Shell.UI.Internal", true);
    var removeAtItemMethod = projectMruListType.GetMethod("RemoveItemAt");
    var itemsProperty = projectMruListType.GetProperty("Items");

    var fileSystemMruItemType = Type.GetType("Microsoft.VisualStudio.PlatformUI.FileSystemMruItem, Microsoft.VisualStudio.Shell.UI.Internal", true);
    var pathProperty = fileSystemMruItemType.GetProperty("Path");

    var items = (IList)itemsProperty.GetValue(projectMruList, null);
    for (var i = items.Count - 1; i > -1; i--)
    {
        var path = (string)pathProperty.GetValue(items[i], null);
        if (!File.Exists(path))
        {
            removeAtItemMethod.Invoke(projectMruList, new object[] { i });
        }
    }
}

朝飯前が晩飯前になってしまいました (;・∀・)

ソースコードはこちらに公開してあります。 github.com