endokのブログ

IT・プログラミングネタ

VisualStudio拡張機能を開発してのTipsまとめ

Tips

「○○へのアクセスはメイン スレッドでのみ行う必要があります。まず Microsoft.VisualStudio.ProjectSystem.IProjectThreadingService.VerifyOnUIThread() を呼び出します。」の警告対策

作ってる側としては問題ない気でもこの警告がガンガンでてくる。
この警告が出てくる箇所の前で

ThreadHelper.ThrowIfNotOnUIThread();

を呼び出す。
メソッド単位で出てくるので大変。この対応で合っているのかは不明・・。

コンテキストメニューを追加する

エディタ部分で右クリックしたときのメニュー項目の足し方。
コマンドを追加した後、vsctファイルにて下記のようにParentのidとして"IDM_VS_CTXT_CODEWIN"を指定する。
表示位置を変えたい場合、priorityの値を調整する。

<Groups>
  <Group guid="guidMyExtensionPackageCmdSet" id="MyMenuGroup" priority="0x0001">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN"/>
  </Group>
</Groups>

特定の条件の場合のみメニューを表示する

vsctファイルにて、デフォルト非表示、表示を動的に設定する。
具体的にはCommandFlagとしてDefaultInvisible、DynamicVisibilityを指定する。

    <Button guid="guidMyExtensionPackageCmdSet" id="ConfigJumpCommandId" priority="0x0100" type="Button">
      <Parent guid="guidMyExtensionPackageCmdSet" id="MyMenuGroup" />
      <Icon guid="guidImages" id="bmpPic1" />
      <CommandFlag>DefaultInvisible</CommandFlag>
      <CommandFlag>DynamicVisibility</CommandFlag>
      <Strings>
        <ButtonText>Invoke Command</ButtonText>
      </Strings>
    </Button>
  </Buttons>

そして、CommandクラスのOnBeforeQueryStatusにてVisible属性をON/OFFする。

    private void OnBeforeQueryStatus(object sender, EventArgs e)
    {
        var cmd = sender as OleMenuCommand;

        if(表示条件)
        {
            cmd.Visible = true;
        }
        else
        {
            cmd.Visible = false;
        }
    }

DTEを利用する

VisualStudioで開いているソリューションやプロジェクト、開いているファイル等にアクセスするには、DTEオブジェクトを利用する。
DTEオブジェクトはCommandクラスのInitializeAsyncにて以下のように取得する。
※CommandクラスにはDTE型のインスタンス変数_dteを用意してある前提

    public static async Task InitializeAsync(AsyncPackage package)
    {
        // Switch to the main thread - the call to AddCommand in ConfigJumpCommand's constructor requires
        // the UI thread.
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

        OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
        Instance = new MyCommand(package, commandService);
        Instance._dte = package.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
    }

DTEでカーソルのある行のテキストを取得する

選択行を取得、選択範囲を変更することでカーソル行テキストを取得できる。
ただしカーソルの移動を伴うため、利用時には横スクロール位置が移動するなど弊害も出ることがあるので注意。
_dteという変数でDTEオブジェクトを持っているものとする。

      public string GetCurrentLineString()
      {
          ThreadHelper.ThrowIfNotOnUIThread();

          var textDocument = (TextDocument)_dte.ActiveDocument.Object("TextDocument");
          var selection = textDocument.Selection;

          // 元の選択範囲を保持する
          var begin = selection.TextRanges.Item(1).StartPoint;
          var end = selection.TextRanges.Item(selection.TextRanges.Count).EndPoint;

          selection.StartOfLine();
          selection.EndOfLine(true);
          var line = selection.Text;

          // 元の選択範囲を復元する
          selection.MoveToLineAndOffset(begin.Line, begin.LineCharOffset);
          selection.MoveToLineAndOffset(end.Line, end.LineCharOffset, true);

          return line;
      }

DTEからソリューション、プロジェクトにアクセスする

ソリューションから取得するProjectはソリューション直下のもので、ソリューションフォルダも含まれるため注意。
また、ProjectItemもプロジェクト直下のものであるため、プロジェクト内から特定のファイルを探す場合には再帰的に探す必要があるため注意。

    // ソリューションを取得
    var solution = _dte.Solution;

    // ソリューション名
    var solutionFullName = solution.FullName;
    var solutionFileName = solution.FileName;

    // プロジェクトを取得
    var projects = solution.Projects

    foreach(Project project in projects)
    {
        // プロジェクト内のアイテムを取得
        foreach(ProjectItem item in project.ProjectItems)
        {
            // 何か処理をする
        }
    }

ProjectItemを開き特定の行へ移動、選択状態にする

    ProjectItem projectItem = 何らか取得する;
    int lineNo = 行数;

    // 開いてなかったら開く
    if (!projectItem.IsOpen[Constants.vsViewKindAny])
    {
        projectItem.Open();
    }

    // アクティブにする
    projectItem.Document.Activate();

    // 指定の行に移動する
    TextDocument textDocument = (TextDocument)projectItem.Document.Object("TextDocument");
    TextSelection selection = textDocument.Selection;
    selection.GotoLine(lineNo, true);

    // 行を選択状態にする
    selection.StartOfLine();
    selection.EndOfLine(true);

特定のファイルを開き、特定の行へ移動、選択状態にする

_dteという変数でDTEオブジェクトを持っているとする。

    string filePath = 開きたいファイルパス;

    // ファイルを開く
    var window = _dte.ItemOperations.OpenFile(filePath);

    // アクティブにする
    window.Document.Activate();

    // 指定の行に移動する
    TextDocument textDocument = (TextDocument)window.Document.Object("TextDocument");
    TextSelection selection = textDocument.Selection;
    selection.GotoLine(lineNo, true);

    // 行を選択状態にする
    selection.StartOfLine();
    selection.EndOfLine(true);

おわりに

ググってもあまり情報は多くなかったので、同じ境遇の方に参考になれば幸いです。