チュートリアル: 入力候補の表示

入力候補を提供する識別子を定義し、入力候補セッションをトリガーすることによって、言語ベースの入力候補を実装できます。 言語サービスのコンテキストで入力候補を定義し、独自のファイル名の拡張子とコンテンツ タイプを定義して、その型の入力候補だけを表示することができます。 または、既存のコンテンツ タイプ ("plaintext" など) の入力候補をトリガーすることもできます。 このチュートリアルでは、テキスト ファイルのコンテンツ タイプである "plaintext" コンテンツ タイプに対して入力候補をトリガーする方法について説明します。 "text" コンテンツ タイプは、コード ファイルや XML ファイルなど、他のすべてのコンテンツ タイプの先祖です。

通常、入力候補は特定の文字を入力することによってトリガーされます。たとえば、"using" などの識別子の先頭を入力します。 通常、Space キー、Tab キー、または Enter キーを押して選択範囲をコミットすると、閉じます。 キーストローク (IOleCommandTarget インターフェイス) のコマンド ハンドラーと、IVsTextViewCreationListener インターフェイスを実装するハンドラー プロバイダーを使用して、文字を入力するときにトリガーされる IntelliSense 機能を実装できます。 入力候補に含まれる識別子の一覧である入力候補ソースを作成するには、ICompletionSource インターフェイスと入力候補ソース プロバイダー (ICompletionSourceProvider インターフェイス) を実装します。 プロバイダーは、Managed Extensibility Framework (MEF) コンポーネントのパーツです。 ソースとコントローラーのクラスをエクスポートし、サービスとブローカーをインポートする役割を担います。たとえば、テキスト バッファー内の移動を可能にする ITextStructureNavigatorSelectorService や、入力候補セッションをトリガーする ICompletionBroker などです。

このチュートリアルでは、ハードコーディングされた識別子のセットに対して入力候補を実装する方法について説明します。 完全な実装では、言語サービスと言語ドキュメントにそのコンテンツを提供する役割があります。

MEF プロジェクトを作成する

MEF プロジェクトを作成するには

  1. C# VSIX プロジェクトを作成します。 ([新しいプロジェクト] ダイアログで、[Visual C#]、[拡張機能][VSIX プロジェクト] の順に選択します。) ソリューションに CompletionTest という名前を付けます。

  2. プロジェクトに、[エディター分類子] 項目テンプレートを追加します。 詳細については、「エディター項目テンプレートを使用して拡張機能を作成する」を参照してください。

  3. 既存のクラス ファイルを削除します。

  4. 次の参照をプロジェクトに追加し、CopyLocalfalse に設定されていることを確認します。

    Microsoft.VisualStudio.Editor

    Microsoft.VisualStudio.Language.Intellisense

    Microsoft.VisualStudio.OLE.Interop

    Microsoft.VisualStudio.Shell.15.0

    Microsoft.VisualStudio.Shell.Immutable.10.0

    Microsoft.VisualStudio.TextManager.Interop

入力候補ソースの実装

入力候補ソースは、識別子の最初の文字などの入力候補のトリガーをユーザーが入力したときに、一連の識別子を収集し、その内容を入力候補ウィンドウに追加する役割を担います。 この例では、識別子とその説明は、AugmentCompletionSession メソッドでハードコーディングされています。 ほとんどの実際の使用では、言語のパーサーを使用してトークンを取得し、入力候補一覧に入力します。

入力候補ソースを実装するには

  1. クラス ファイルを追加し、その名前を TestCompletionSourceにします。

  2. 次のインポートを追加します。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio.Utilities;
    
  3. ICompletionSource を実装するように、TestCompletionSource のクラス宣言を変更します。

    internal class TestCompletionSource : ICompletionSource
    
  4. ソース プロバイダー、テキスト バッファー、および (入力候補セッションに含まれる識別子に対応する) Completion オブジェクトの一覧のプライベート フィールドを追加します。

    private TestCompletionSourceProvider m_sourceProvider;
    private ITextBuffer m_textBuffer;
    private List<Completion> m_compList;
    
  5. ソース プロバイダーとバッファーを設定するコンストラクターを追加します。 TestCompletionSourceProvider クラスは、後の手順で定義します。

    public TestCompletionSource(TestCompletionSourceProvider sourceProvider, ITextBuffer textBuffer)
    {
        m_sourceProvider = sourceProvider;
        m_textBuffer = textBuffer;
    }
    
  6. コンテキストで提供する入力候補を含む入力候補セットを追加して、AugmentCompletionSession メソッドを実装します。 各入力候補セットには Completion 入力候補のセットが含まれており、入力候補ウィンドウのタブに対応しています。 (Visual Basic プロジェクトでは、[入力候補] ウィンドウのタブの名前は 共通すべて になります。) FindTokenSpanAtPosition メソッドは、次の手順で定義します。

    void ICompletionSource.AugmentCompletionSession(ICompletionSession session, IList<CompletionSet> completionSets)
    {
        List<string> strList = new List<string>();
        strList.Add("addition");
        strList.Add("adaptation");
        strList.Add("subtraction");
        strList.Add("summation");
        m_compList = new List<Completion>();
        foreach (string str in strList)
            m_compList.Add(new Completion(str, str, str, null, null));
    
        completionSets.Add(new CompletionSet(
            "Tokens",    //the non-localized title of the tab
            "Tokens",    //the display title of the tab
            FindTokenSpanAtPosition(session.GetTriggerPoint(m_textBuffer),
                session),
            m_compList,
            null));
    }
    
  7. カーソルの位置から現在の単語を検索するには、次のメソッドを使用します。

    private ITrackingSpan FindTokenSpanAtPosition(ITrackingPoint point, ICompletionSession session)
    {
        SnapshotPoint currentPoint = (session.TextView.Caret.Position.BufferPosition) - 1;
        ITextStructureNavigator navigator = m_sourceProvider.NavigatorService.GetTextStructureNavigator(m_textBuffer);
        TextExtent extent = navigator.GetExtentOfWord(currentPoint);
        return currentPoint.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive);
    }
    
  8. Dispose() メソッドを実装します。

    private bool m_isDisposed;
    public void Dispose()
    {
        if (!m_isDisposed)
        {
            GC.SuppressFinalize(this);
            m_isDisposed = true;
        }
    }
    

入力候補ソース プロバイダーを実装します。

入力候補ソース プロバイダーは、入力候補ソースをインスタンス化する MEF コンポーネントのパーツです。

入力候補ソース プロバイダーを実装するには

  1. ICompletionSourceProvider を実装する、TestCompletionSourceProvider という名前のクラスを追加します。 ContentTypeAttribute を "plaintext"、NameAttribute を "test completion" として、このクラスをエクスポートします。

    [Export(typeof(ICompletionSourceProvider))]
    [ContentType("plaintext")]
    [Name("token completion")]
    internal class TestCompletionSourceProvider : ICompletionSourceProvider
    
  2. ITextStructureNavigatorSelectorService をインポートします。これにより、入力候補ソース内の現在の単語が検索されます。

    [Import]
    internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
    
  3. TryCreateCompletionSource メソッドを実装して、入力候補ソースをインスタンス化します。

    public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer)
    {
        return new TestCompletionSource(this, textBuffer);
    }
    

入力候補コマンド ハンドラー プロバイダーを実装する

入力候補コマンド ハンドラー プロバイダーは、IVsTextViewCreationListener から派生します。このクラスは、テキスト ビュー作成イベントをリッスンし、IVsTextView から、Visual Studio シェルのコマンド チェーンにコマンドを追加できるように、そのビューを ITextView に変換します。 このクラスは MEF エクスポートであるため、このクラスを使用して、コマンド ハンドラー自体が必要とするサービスをインポートすることもできます。

入力候補コマンド ハンドラー プロバイダーを実装するには

  1. TestCompletionCommandHandler という名前のファイルを追加します。

  2. 次の using ディレクティブを追加します。

    using System;
    using System.ComponentModel.Composition;
    using System.Runtime.InteropServices;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.VisualStudio.Utilities;
    
  3. IVsTextViewCreationListener を実装する、TestCompletionHandlerProvider という名前のクラスを追加します。 NameAttribute を "token completion handler"、ContentTypeAttribute を "plaintext"、および TextViewRoleAttributeEditable として、このクラスをエクスポートします。

    [Export(typeof(IVsTextViewCreationListener))]
    [Name("token completion handler")]
    [ContentType("plaintext")]
    [TextViewRole(PredefinedTextViewRoles.Editable)]
    internal class TestCompletionHandlerProvider : IVsTextViewCreationListener
    
  4. IVsEditorAdaptersFactoryService をインポートします。これにより、IVsTextView から ITextViewICompletionBroker、および SVsServiceProvider への変換が可能になり、標準の Visual Studio サービスへのアクセスが可能になります。

    [Import]
    internal IVsEditorAdaptersFactoryService AdapterService = null;
    [Import]
    internal ICompletionBroker CompletionBroker { get; set; }
    [Import]
    internal SVsServiceProvider ServiceProvider { get; set; }
    
  5. VsTextViewCreated メソッドを実装して、コマンド ハンドラーをインスタンス化します。

    public void VsTextViewCreated(IVsTextView textViewAdapter)
    {
        ITextView textView = AdapterService.GetWpfTextView(textViewAdapter);
        if (textView == null)
            return;
    
        Func<TestCompletionCommandHandler> createCommandHandler = delegate() { return new TestCompletionCommandHandler(textViewAdapter, textView, this); };
        textView.Properties.GetOrCreateSingletonProperty(createCommandHandler);
    }
    

入力候補コマンド ハンドラーを実装する

入力候補はキーストロークによってトリガーされるため、IOleCommandTarget インターフェイスを実装して、入力候補セッションをトリガー、コミット、および破棄するキーストロークを受信して処理する必要があります。

入力候補コマンド ハンドラーを実装するには

  1. IOleCommandTarget を実装する、TestCompletionCommandHandler という名前のクラスを追加します。

    internal class TestCompletionCommandHandler : IOleCommandTarget
    
  2. 次のコマンド ハンドラー (コマンドの渡し先)、テキスト ビュー、コマンド ハンドラー プロバイダー (さまざまなサービスへのアクセスを可能にします)、および入力候補セッションのプライベート フィールドを追加します。

    private IOleCommandTarget m_nextCommandHandler;
    private ITextView m_textView;
    private TestCompletionHandlerProvider m_provider;
    private ICompletionSession m_session;
    
  3. テキスト ビューとプロバイダー フィールドを設定するコンストラクターを追加し、コマンド チェーンにコマンドを追加します。

    internal TestCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, TestCompletionHandlerProvider provider)
    {
        this.m_textView = textView;
        this.m_provider = provider;
    
        //add the command to the command chain
        textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler);
    }
    
  4. 次のコマンドを渡すことによって、QueryStatus メソッドを実装します。

    public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
    {
        return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
    }
    
  5. Exec メソッドを実装します。 このメソッドがキーストロークを受け取ったとき、次のいずれかの操作を行う必要があります。

    • 文字がバッファーに書き込まれることを許可し、その後、入力候補をトリガーまたはフィルター処理します。 (印刷文字はこれを行います。)

    • 入力候補をコミットしますが、バッファーへの文字の書き込みは許可しません。 (入力候補セッションが表示されるとき、Space キー、Tab キー、または Enter キー がこれを行います。)

    • コマンドを次のハンドラーに渡すことを許可します。 (その他のすべてのコマンド。)

      このメソッドは UI を表示する可能性があるため、IsInAutomationFunction を呼び出して、オートメーション コンテキストで呼び出されないようにします。

      public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
      {
          if (VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider))
          {
              return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
          }
          //make a copy of this so we can look at it after forwarding some commands
          uint commandID = nCmdID;
          char typedChar = char.MinValue;
          //make sure the input is a char before getting it
          if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)
          {
              typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
          }
      
          //check for a commit character
          if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN
              || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB
              || (char.IsWhiteSpace(typedChar) || char.IsPunctuation(typedChar)))
          {
              //check for a selection
              if (m_session != null && !m_session.IsDismissed)
              {
                  //if the selection is fully selected, commit the current session
                  if (m_session.SelectedCompletionSet.SelectionStatus.IsSelected)
                  {
                      m_session.Commit();
                      //also, don't add the character to the buffer
                      return VSConstants.S_OK;
                  }
                  else
                  {
                      //if there is no selection, dismiss the session
                      m_session.Dismiss();
                  }
              }
          }
      
          //pass along the command so the char is added to the buffer
          int retVal = m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
          bool handled = false;
          if (!typedChar.Equals(char.MinValue) && char.IsLetterOrDigit(typedChar))
          {
              if (m_session == null || m_session.IsDismissed) // If there is no active session, bring up completion
              {
                  this.TriggerCompletion();
                  m_session.Filter();
              }
              else    //the completion session is already active, so just filter
              {
                  m_session.Filter();
              }
              handled = true;
          }
          else if (commandID == (uint)VSConstants.VSStd2KCmdID.BACKSPACE   //redo the filter if there is a deletion
              || commandID == (uint)VSConstants.VSStd2KCmdID.DELETE)
          {
              if (m_session != null && !m_session.IsDismissed)
                  m_session.Filter();
              handled = true;
          }
          if (handled) return VSConstants.S_OK;
          return retVal;
      }
      

  6. このコードは、入力候補セッションをトリガーするプライベート メソッドです。

    private bool TriggerCompletion()
    {
        //the caret must be in a non-projection location 
        SnapshotPoint? caretPoint =
        m_textView.Caret.Position.Point.GetPoint(
        textBuffer => (!textBuffer.ContentType.IsOfType("projection")), PositionAffinity.Predecessor);
        if (!caretPoint.HasValue)
        {
            return false;
        }
    
        m_session = m_provider.CompletionBroker.CreateCompletionSession
     (m_textView,
            caretPoint.Value.Snapshot.CreateTrackingPoint(caretPoint.Value.Position, PointTrackingMode.Positive),
            true);
    
        //subscribe to the Dismissed event on the session 
        m_session.Dismissed += this.OnSessionDismissed;
        m_session.Start();
    
        return true;
    }
    
  7. 次の例は、Dismissed イベントからアンサブスクライブするプライベート メソッドです。

    private void OnSessionDismissed(object sender, EventArgs e)
    {
        m_session.Dismissed -= this.OnSessionDismissed;
        m_session = null;
    }
    

コードのビルドとテスト

このコードをテストするには、CompletionTest ソリューションをビルドし、実験用インスタンスで実行します。

CompletionTest ソリューションをビルドしてテストするには

  1. ソリューションをビルドします。

  2. デバッガーでこのプロジェクトを実行すると、Visual Studio の 2 つ目のインスタンスが起動されます。

  3. テキスト ファイルを作成し、"add" という単語を含む何らかのテキストを入力します。

  4. 最初に "a"、次に "d" を入力すると、"addition" と "adaptation" を含む一覧が表示されます。 addition が選択されていることを確認します。 もう 1 回 "d" を入力すると、一覧に "addition" のみが表示されます。これは現在選択されています。 Space キー、Tab キー、または Enter キーを押して "addition" をコミットするか、Esc キーまたは他のキーを入力して一覧を閉じます。