共用方式為


逐步解說:顯示陳述式完成

透過定義想要提供完成的識別碼,然後觸發完成工作階段,即可實作以語言為基礎的陳述式完成。 您可以在語言服務的內容中定義陳述式完成、定義您自己的副檔名和內容類型,然後只顯示該類型的完成。 或者,您可以觸發現有內容類型的完成,例如「純文字」。 本逐步解說示範如何觸發「純文字」內容類型 (文字檔案的內容類型) 的陳述式完成。 「文字」內容類型是所有其他內容類型的上階,包括程式碼和 XML 檔案。

陳述式完成通常是由輸入特定字元觸發,例如,輸入識別碼的開頭,例如「using」。 它通常會在按下空白鍵Tab,或 Enter 鍵認可選取時關閉。 您可以透過使用按鍵輸入的命令處理常式(IOleCommandTarget 介面) 和實作 IVsTextViewCreationListener 介面的處理常式提供者,來實作在輸入字元時觸發的 IntelliSense 功能。 若要建立完成來源,即參與完成的識別碼清單,請實作 ICompletionSource 介面和完成來源提供者 (ICompletionSourceProvider 介面)。 提供者是 Managed Extensibility Framework (MEF) 元件部分。 它們負責匯出來源和控制器類別,並匯入服務和訊息代理程式,例如 ITextStructureNavigatorSelectorService (可啟用文字緩衝區中的瀏覽),以及 ICompletionBroker (觸發完成工作階段) 。

本逐步解說示範如何為一組硬式編碼識別碼實作陳述式完成。 在完整實作中,語言服務和語言文件負責提供該內容。

建立 MEF 專案

建立 MEF 專案

  1. 建立 C# VXIS 專案。 (在新增專案對話框中,選取 Visual C# /擴充性,然後選取 VSIX 專案)。命名解決方案 CompletionTest

  2. 將編輯器分類器項目範本新增至專案。 如需詳細資訊,請參閱 使用編輯器項目範本建立擴充功能

  3. 刪除現有類別檔案。

  4. 將下列參考新增到專案並確定將 CopyLocal 設定為 false

    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. 修改 TestCompletionSource 的類別宣告,使其實作 ICompletionSource

    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 專案中,完成視窗索引標籤會命名為 CommonAll)。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. 新增名為 TestCompletionSourceProvider 且實作 ICompletionSourceProvider 的類別。 使用「純文字」的 ContentTypeAttribute 和「測試完成」的 NameAttribute 來匯出此類別。

    [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. 新增名為 TestCompletionHandlerProvider 且實作 IVsTextViewCreationListener 的類別。 使用「權杖處理常式」的 NameAttribute、「純文字」的 ContentTypeAttribute,和 EditableTextViewRoleAttribute 匯出此類別。

    [Export(typeof(IVsTextViewCreationListener))]
    [Name("token completion handler")]
    [ContentType("plaintext")]
    [TextViewRole(PredefinedTextViewRoles.Editable)]
    internal class TestCompletionHandlerProvider : IVsTextViewCreationListener
    
  4. 匯入 IVsEditorAdaptersFactoryService,其可從 IVsTextView 轉換成 ITextViewICompletionBroker,和能夠存取標準 Visual Studio 服務的 SVsServiceProvider

    [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. 新增名為 TestCompletionCommandHandler 且實作 IOleCommandTarget 的類別:

    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 方法。 當此方法收到按鍵輸入時,它必須執行下列其中一項動作:

    • 允許將字元寫入緩衝區,然後觸發或篩選完成。 (就如列印字元。)

    • 認可完成,但不允許將字元寫入緩衝區。 (空白字元, 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 執行個體。

  3. 建立文字檔,並輸入一些包含 「add」 單字的文字。

  4. 當您輸入第一個 [a] 然後輸入 [d] 時,應該會出現包含 [addition] 和 [adaptation] 的清單。 請注意,已選取該 [addition]。 當您輸入另一個 [d] 時,清單應只包含已選取的 [addition]。 您可以按 空白鍵Tab,或 Enter 鍵來認可 [addition],或按 Esc 或任何其他鍵來關閉清單。