演练:显示语句完成

可以通过定义要为其提供完成的标识符,然后触发完成会话来实现基于语言的语句完成。 可以在语言服务的上下文中定义语句完成,定义自己的文件扩展名和内容类型,然后仅显示该类型的完成。 或者,可以触发现有内容类型(例如“纯文本”)的完成。 本演练演示如何为“纯文本”内容类型(文本文件的内容类型)触发语句完成。 “text”内容类型是所有其他内容类型的上级,包括代码和 XML 文件。

语句完成通常通过键入某些字符(例如,键入标识符(如“using”)的开头来触发。 通常通过按 空格键TabEnter 键提交所选内容来消除它。 通过使用键击( IOleCommandTarget 接口)的命令处理程序和实现 IVsTextViewCreationListener 接口的处理程序提供程序,可以实现在键入字符时触发的 IntelliSense 功能。 若要创建完成源(即参与完成的标识符列表),请实现 ICompletionSource 接口和完成源提供程序( ICompletionSourceProvider 接口)。 提供程序是托管扩展性框架(MEF)组件部件。 它们负责导出源和控制器类以及导入服务和中转站(例如 ITextStructureNavigatorSelectorService,在文本缓冲区中启用导航)以及 ICompletionBroker触发完成会话的导航。

本演练演示如何为硬编码的标识符集实现语句完成。 在完整的实现中,语言服务和语言文档负责提供该内容。

创建 MEF 项目

创建 MEF 项目

  1. 创建 C# VSIX 项目。 (在 “新建项目 ”对话框,选择 Visual C# /扩展性,然后选择 VSIX Project。)将解决方案 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. 修改其实现的类声明TestCompletionSourceICompletionSource

    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. 添加一个名为 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 shell 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从 a 到 a IVsTextView ITextView、a ICompletionBrokerSVsServiceProvider 启用对标准 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. 添加一个名为 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 方法。 此方法收到击键时,必须执行以下操作之一:

    • 允许将字符写入缓冲区,然后触发或筛选完成。 (打印字符执行此操作。

    • 提交完成,但不允许将字符写入缓冲区。 (空格, 选项卡,并在 显示完成会话时输入 执行此操作。

    • 允许将命令传递给下一个处理程序。 (所有其他命令。

      由于此方法可能显示 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”时,应显示包含“添加”和“适应”的列表。 请注意,已选择添加。 键入另一个“d”时,列表应仅包含“添加”,现在已选中。 可以通过按 空格键Tab 键或 Enter 键提交“添加”,也可以键入 Esc 或任何其他键来消除列表。