チュートリアル: コード スニペットを実装する

コード スニペットを作成してエディター拡張機能に含めると、拡張機能のユーザーがそれらを自分のコードに追加できるようになります。

コード スニペットは、ファイルに組み込むことができるコードまたはその他のテキストの断片です。 特定のプログラミング言語用に登録されているすべてのスニペットを表示するには、[ツール] メニューの [コード スニペット マネージャー] をクリックします。 スニペットをファイルに挿入するには、スニペットを配置する場所を右クリックし、[スニペットの挿入] または [ブロックの挿入] をクリックして、目的のスニペットを見つけてダブルクリックします。 Tab キーまたは Shift+Tab キーを押して、スニペットの関連部分を変更した後、Enter キーまたは Esc キーを押して変更を受け入れます。 詳しくは、「コード スニペット」をご覧ください。

コード スニペットは、.snippet* というファイル名拡張子を持つ XML ファイルに格納されます。 スニペットには、スニペットの挿入後に強調表示されるフィールドを含めることができます。これにより、ユーザーはそれらを見つけて変更できるようになります。 スニペット ファイルでは、コード スニペット マネージャー用の情報も提供されます。これにより、スニペット名が正しいカテゴリに表示されるようになります。 スニペット スキーマについて詳しくは、「コード スニペット スキーマ リファレンス」をご覧ください。

このチュートリアルでは、次のタスクを実行する方法を説明します。

  1. 特定の言語用のコード スニペットを作成して登録します。

  2. [スニペットの挿入] コマンドをショートカット メニューに追加します。

  3. スニペット拡張を実装します。

    このチュートリアルは、「チュートリアル: 入力候補の表示」に基づいています。

コードスニペットを作成して登録する

通常、コード スニペットは、登録されている言語サービスに関連付けられます。 ただし、コード スニペットを登録するために LanguageService を実装する必要はありません。 代わりに、スニペットのインデックス ファイルで GUID を指定し、プロジェクトに追加する ProvideLanguageCodeExpansionAttribute で同じ GUID を使用します。

次の手順では、コード スニペットを作成し、特定の GUID に関連付ける方法を示します。

  1. 次のディレクトリ構造を作成します。

    %InstallDir%\TestSnippets\Snippets\1033\

    ここで、%InstallDir% は Visual Studio のインストール フォルダーです (通常は、このパスがコード スニペットのインストールに使用されますが、任意のパスを指定することもできます)。

  2. \1033\ フォルダーで、.xml ファイルを作成し、名前を TestSnippets.xml と指定します。 (通常は、この名前がスニペットのインデックス ファイルに使用されますが、ファイル名拡張子が .xml であれば、任意の名前を指定することもできます)。次のテキストを追加し、プレースホルダー GUID を削除して、独自の GUID を追加します。

    <?xml version="1.0" encoding="utf-8" ?>
    <SnippetCollection>
        <Language Lang="TestSnippets" Guid="{00000000-0000-0000-0000-000000000000}">
            <SnippetDir>
                <OnOff>On</OnOff>
                <Installed>true</Installed>
                <Locale>1033</Locale>
                <DirPath>%InstallRoot%\TestSnippets\Snippets\%LCID%\</DirPath>
                <LocalizedName>Snippets</LocalizedName>
            </SnippetDir>
        </Language>
    </SnippetCollection>
    
  3. スニペット フォルダー内にファイルを作成し、名前を test.snippet と指定して、次のテキストを追加します。

    <?xml version="1.0" encoding="utf-8" ?>
    <CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
        <CodeSnippet Format="1.0.0">
            <Header>
                <Title>Test replacement fields</Title>
                <Shortcut>test</Shortcut>
                <Description>Code snippet for testing replacement fields</Description>
                <Author>MSIT</Author>
                <SnippetTypes>
                    <SnippetType>Expansion</SnippetType>
                </SnippetTypes>
            </Header>
            <Snippet>
                <Declarations>
                    <Literal>
                      <ID>param1</ID>
                        <ToolTip>First field</ToolTip>
                        <Default>first</Default>
                    </Literal>
                    <Literal>
                        <ID>param2</ID>
                        <ToolTip>Second field</ToolTip>
                        <Default>second</Default>
                    </Literal>
                </Declarations>
                <References>
                   <Reference>
                       <Assembly>System.Windows.Forms.dll</Assembly>
                   </Reference>
                </References>
                <Code Language="TestSnippets">
                    <![CDATA[MessageBox.Show("$param1$");
         MessageBox.Show("$param2$");]]>
                </Code>
            </Snippet>
        </CodeSnippet>
    </CodeSnippets>
    

    次の手順は、コード スニペットの登録方法を示しています。

特定の GUID のコード スニペットを登録するには

  1. CompletionTest プロジェクトを開きます。 このプロジェクトの作成方法について詳しくは、「チュートリアル: 入力候補の表示」をご覧ください。

  2. プロジェクトで、次のアセンブリへの参照を追加します。

    • Microsoft.VisualStudio.TextManager.Interop

    • Microsoft.VisualStudio.TextManager.Interop.8.0

    • microsoft.msxml

  3. プロジェクトで、source.extension.vsixmanifest ファイルを開きます。

  4. [アセット] タブに VsPackage コンテンツ タイプが含まれていて、その [プロジェクト] がプロジェクトの名前に設定されていることを確認します。

  5. CompletionTest プロジェクトを選択し、プロパティ ウィンドウで [Pkgdef ファイルの生成]true に設定します。 プロジェクトを保存します。

  6. 静的 SnippetUtilities クラスをプロジェクトに追加します。

    static class SnippetUtilities
    
  7. SnippetUtilities クラスで、GUID を定義し、SnippetsIndex.xml ファイルで使用した値を指定します。

    internal const string LanguageServiceGuidStr = "00000000-0000-0000-0000-00000000";
    
  8. ProvideLanguageCodeExpansionAttributeTestCompletionHandler クラスに追加します。 この属性は、プロジェクト内の任意のパブリックまたは内部 (非静的) クラスに追加できます (場合によっては、Microsoft.VisualStudio.Shell 名前空間用の using ディレクティブを追加する必要があります)。

    [ProvideLanguageCodeExpansion(
    SnippetUtilities.LanguageServiceGuidStr,
    "TestSnippets", //the language name
    0,              //the resource id of the language
    "TestSnippets", //the language ID used in the .snippet files
    @"%InstallRoot%\TestSnippets\Snippets\%LCID%\TestSnippets.xml",
        //the path of the index file
    SearchPaths = @"%InstallRoot%\TestSnippets\Snippets\%LCID%\",
    ForceCreateDirs = @"%InstallRoot%\TestSnippets\Snippets\%LCID%\")]
    internal class TestCompletionCommandHandler : IOleCommandTarget
    
  9. プロジェクトをビルドして実行します。 プロジェクトの実行時に起動される Visual Studio の実験用インスタンスで、先ほど登録したスニペットが TestSnippets 言語の下のコード スニペット マネージャーに表示されます。

[スニペットの挿入] コマンドをショートカット メニューに追加する

[スニペットの挿入] コマンドは、テキスト ファイル用のショートカット メニューには含まれません。 そのため、コマンドを有効にする必要があります。

[スニペットの挿入] コマンドをショートカット メニューに追加するには

  1. クラス ファイルを TestCompletionCommandHandler 開きます。

    このクラスは IOleCommandTarget を実装しているため、QueryStatus メソッドで [スニペットの挿入] コマンドをアクティブにすることができます。 コマンドを有効にする前に、このメソッドがオートメーション関数の内部で呼び出されないことを確認してください。これは、[スニペットの挿入] コマンドをクリックしたときに、スニペット ピッカー ユーザー インターフェイス (UI) が表示されるためです。

    public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
    {
        if (!VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider))
        {
            if (pguidCmdGroup == VSConstants.VSStd2K && cCmds > 0)
            {
                // make the Insert Snippet command appear on the context menu 
                if ((uint)prgCmds[0].cmdID == (uint)VSConstants.VSStd2KCmdID.INSERTSNIPPET)
                {
                    prgCmds[0].cmdf = (int)Constants.MSOCMDF_ENABLED | (int)Constants.MSOCMDF_SUPPORTED;
                    return VSConstants.S_OK;
                }
            }
        }
    
        return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
    }
    
  2. プロジェクトをビルドして実行します。 実験用インスタンスで、ファイル名拡張子が .zzz のファイルを開き、その中の任意の場所を右クリックします。 ショートカット メニューに [スニペットの挿入] コマンドが表示されます。

スニペット ピッカー UI でスニペット拡張を実装する

このセクションでは、コード スニペット拡張を実装して、ショートカット メニューの [スニペットの挿入] をクリックしたときに、スニペット ピッカー UI が表示されるようにする方法について説明します。 コード スニペットは、ユーザーがコード スニペットのショートカットを入力し、Tab キーを押したときにも展開されます。

スニペット ピッカー UI を表示し、ナビゲーションと挿入後のスニペットの受け入れを有効にするには、Exec メソッドを使用します。 挿入自体は、OnItemChosen メソッドによって処理されます。

コード スニペット拡張の実装では、レガシの Microsoft.VisualStudio.TextManager.Interop インターフェイスが使用されます。 現在のエディター クラスからレガシ コードに変換する場合、レガシ インターフェイスでは、行番号と列番号の組み合わせを使用してテキスト バッファー内の場所が指定されますが、現在のクラスでは、1 つのインデックスが使用されることに注意してください。 したがって、バッファー内に、それぞれ 10 文字 (および 1 文字としてカウントされる改行) を含む 3 つの行がある場合、3 行目の 4 番目の文字は、現在の実装では 27 の位置になりますが、以前の実装では、2 行目の 3 の位置になります。

スニペット拡張を実装するには

  1. TestCompletionCommandHandler クラスを含むファイルに、次の using ディレクティブを追加します。

    using Microsoft.VisualStudio.Text.Operations;
    using MSXML;
    using System.ComponentModel.Composition;
    
  2. TestCompletionCommandHandler クラスに IVsExpansionClient インターフェイスを実装させます。

    internal class TestCompletionCommandHandler : IOleCommandTarget, IVsExpansionClient
    
  3. TestCompletionCommandHandlerProvider クラスで、ITextStructureNavigatorSelectorService をインポートします。

    [Import]
    internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
    
  4. コード拡張インターフェイスと IVsTextView 用のプライベート フィールドを追加します。

    IVsTextView m_vsTextView;
    IVsExpansionManager m_exManager;
    IVsExpansionSession m_exSession;
    
  5. TestCompletionCommandHandler クラスのコンストラクターで、次のフィールドを設定します。

    internal TestCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, TestCompletionHandlerProvider provider)
    {
        this.m_textView = textView;
        m_vsTextView = textViewAdapter;
        m_provider = provider;
        //get the text manager from the service provider
        IVsTextManager2 textManager = (IVsTextManager2)m_provider.ServiceProvider.GetService(typeof(SVsTextManager));
        textManager.GetExpansionManager(out m_exManager);
        m_exSession = null;
    
        //add the command to the command chain
        textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler);
    }
    
  6. ユーザーが [スニペットの挿入] コマンドをクリックしたときにスニペット ピッカーを表示するには、Exec メソッドに次のコードを追加します (ここでは、説明を読みやすくするために、ステートメント入力候補に使用される Exec() コードは表示しません。代わりに、既存のメソッドにコードのブロックが追加されます)。文字をチェックするコードの後に、次のコード ブロックを追加します。

    //code previously written for Exec
    if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)
    {
        typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
    }
    //the snippet picker code starts here
    if (nCmdID == (uint)VSConstants.VSStd2KCmdID.INSERTSNIPPET)
    {
        IVsTextManager2 textManager = (IVsTextManager2)m_provider.ServiceProvider.GetService(typeof(SVsTextManager));
    
        textManager.GetExpansionManager(out m_exManager);
    
        m_exManager.InvokeInsertionUI(
            m_vsTextView,
            this,      //the expansion client
            new Guid(SnippetUtilities.LanguageServiceGuidStr),
            null,       //use all snippet types
            0,          //number of types (0 for all)
            0,          //ignored if iCountTypes == 0
            null,       //use all snippet kinds
            0,          //use all snippet kinds
            0,          //ignored if iCountTypes == 0
            "TestSnippets", //the text to show in the prompt
            string.Empty);  //only the ENTER key causes insert 
    
        return VSConstants.S_OK;
    }
    
  7. 移動できるフィールドがスニペットにある場合、拡張が明示的に許可されるまで、拡張セッションは開いたままになります。スニペットにフィールドがない場合、セッションは閉じられ、InvokeInsertionUI メソッドによって null として返されます。 Exec メソッドで、前の手順で追加したスニペット ピッカー UI コードの後に、スニペット ナビゲーションを処理する次のコードを追加します (ユーザーがスニペットの挿入後に Tab または Shift+Tab キーを押した場合)。

    //the expansion insertion is handled in OnItemChosen
    //if the expansion session is still active, handle tab/backtab/return/cancel
    if (m_exSession != null)
    {
        if (nCmdID == (uint)VSConstants.VSStd2KCmdID.BACKTAB)
        {
            m_exSession.GoToPreviousExpansionField();
            return VSConstants.S_OK;
        }
        else if (nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB)
        {
    
            m_exSession.GoToNextExpansionField(0); //false to support cycling through all the fields
            return VSConstants.S_OK;
        }
        else if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN || nCmdID == (uint)VSConstants.VSStd2KCmdID.CANCEL)
        {
            if (m_exSession.EndCurrentExpansion(0) == VSConstants.S_OK)
            {
                m_exSession = null;
                return VSConstants.S_OK;
            }
        }
    }
    
  8. ユーザーが対応するショートカットを入力し、Tab キーを押したときにコード スニペットを挿入するには、Exec メソッドにコードを追加します。 スニペットを挿入するプライベート メソッドは、後の手順で示します。 前の手順で追加したナビゲーション コードの後に、次のコードを追加します。

    //neither an expansion session nor a completion session is open, but we got a tab, so check whether the last word typed is a snippet shortcut 
    if (m_session == null && m_exSession == null && nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB)
    {
        //get the word that was just added 
        CaretPosition pos = m_textView.Caret.Position;
        TextExtent word = m_provider.NavigatorService.GetTextStructureNavigator(m_textView.TextBuffer).GetExtentOfWord(pos.BufferPosition - 1); //use the position 1 space back
        string textString = word.Span.GetText(); //the word that was just added
        //if it is a code snippet, insert it, otherwise carry on
        if (InsertAnyExpansion(textString, null, null))
            return VSConstants.S_OK;
    }
    
  9. IVsExpansionClient インターフェイスのメソッドを実装します。 この実装では、目的のメソッドは EndExpansionOnItemChosen だけです。 他のメソッドは S_OK を返すだけです。

    public int EndExpansion()
    {
        m_exSession = null;
        return VSConstants.S_OK;
    }
    
    public int FormatSpan(IVsTextLines pBuffer, TextSpan[] ts)
    {
        return VSConstants.S_OK;
    }
    
    public int GetExpansionFunction(IXMLDOMNode xmlFunctionNode, string bstrFieldName, out IVsExpansionFunction pFunc)
    {
        pFunc = null;
        return VSConstants.S_OK;
    }
    
    public int IsValidKind(IVsTextLines pBuffer, TextSpan[] ts, string bstrKind, out int pfIsValidKind)
    {
        pfIsValidKind = 1;
        return VSConstants.S_OK;
    }
    
    public int IsValidType(IVsTextLines pBuffer, TextSpan[] ts, string[] rgTypes, int iCountTypes, out int pfIsValidType)
    {
        pfIsValidType = 1;
        return VSConstants.S_OK;
    }
    
    public int OnAfterInsertion(IVsExpansionSession pSession)
    {
        return VSConstants.S_OK;
    }
    
    public int OnBeforeInsertion(IVsExpansionSession pSession)
    {
        return VSConstants.S_OK;
    }
    
    public int PositionCaretForEditing(IVsTextLines pBuffer, TextSpan[] ts)
    {
        return VSConstants.S_OK;
    }
    
  10. OnItemChosen メソッドを実装します。 拡張を実際に挿入するヘルパー メソッドについては、後の手順で説明します。 TextSpan は行と列の情報を提供します。この情報は IVsTextView から取得できます。

    public int OnItemChosen(string pszTitle, string pszPath)
    {
        InsertAnyExpansion(null, pszTitle, pszPath);
        return VSConstants.S_OK;
    }
    
  11. 次のプライベート メソッドでは、ショートカットまたはタイトルとパスに基づいて、コード スニペットが挿入されます。 その後、スニペットを使って InsertNamedExpansion メソッドが呼び出されます。

    private bool InsertAnyExpansion(string shortcut, string title, string path)
    {
        //first get the location of the caret, and set up a TextSpan
        int endColumn, startLine;
        //get the column number from  the IVsTextView, not the ITextView
        m_vsTextView.GetCaretPos(out startLine, out endColumn);
    
        TextSpan addSpan = new TextSpan();
        addSpan.iStartIndex = endColumn;
        addSpan.iEndIndex = endColumn;
        addSpan.iStartLine = startLine;
        addSpan.iEndLine = startLine;
    
        if (shortcut != null) //get the expansion from the shortcut
        {
            //reset the TextSpan to the width of the shortcut, 
            //because we're going to replace the shortcut with the expansion
            addSpan.iStartIndex = addSpan.iEndIndex - shortcut.Length;
    
            m_exManager.GetExpansionByShortcut(
                this,
                new Guid(SnippetUtilities.LanguageServiceGuidStr),
                shortcut,
                m_vsTextView,
                new TextSpan[] { addSpan },
                0,
                out path,
                out title);
    
        }
        if (title != null && path != null)
        {
            IVsTextLines textLines;
            m_vsTextView.GetBuffer(out textLines);
            IVsExpansion bufferExpansion = (IVsExpansion)textLines;
    
            if (bufferExpansion != null)
            {
                int hr = bufferExpansion.InsertNamedExpansion(
                    title,
                    path,
                    addSpan,
                    this,
                    new Guid(SnippetUtilities.LanguageServiceGuidStr),
                    0,
                   out m_exSession);
                if (VSConstants.S_OK == hr)
                {
                    return true;
                }
            }
        }
        return false;
    }
    

コード スニペット拡張をビルドしてテストする

プロジェクトでスニペット拡張が機能するかどうかをテストできます。

  1. ソリューションをビルドします。 デバッガーでこのプロジェクトを実行すると、Visual Studio の 2 つ目のインスタンスが起動されます。

  2. テキスト ファイルを開き、いくつかのテキストを入力します。

  3. テキスト内の任意の場所を右クリックし、[スニペットの挿入] をクリックします。

  4. スニペット ピッカー UI が表示され、「Test replacement fields」という文字列ポップアップが表示されます。 ポップアップをダブルクリックします。

    次のスニペットが挿入されます。

    MessageBox.Show("first");
    MessageBox.Show("second");
    

    Enter キーや Esc キーは押さないでください。

  5. Tab キーまたは Shift+Tab キーを押して、"first" と "second" を切り替えます。

  6. Enter キーまたは Esc キーを押して、挿入を受け入れます。

  7. テキストの別の部分に「test」と入力し、Tab キーを押します。"test" はコード スニペットのショートカットなので、スニペットが再び挿入されます。