演练:实现代码片段

可以创建代码片段并将其包含在编辑器扩展中,以便扩展的用户可以将它们添加到自己的代码中。

代码片段是可以合并到文件中的代码片段或其他文本。 若要查看已注册特定编程语言的所有代码段,请在 “工具” 菜单上,单击“ 代码段管理器”。 若要在文件中插入代码段,请右键单击所需代码段的位置,单击“插入代码段”或 “环绕”,找到所需的代码段,然后双击它。 按 TabShift+Tab 可修改代码片段的相关部分,然后按 EnterEsc 接受它。 有关详细信息,请参阅 代码片段

代码片段包含在扩展名为 .snippet* 的 XML 文件中。 代码片段可以包含插入代码段后突出显示的字段,以便用户可以查找和更改它们。 代码段文件还提供代码片段管理器的信息,以便它可以在正确的类别中显示代码段名称。 有关代码段架构的信息,请参阅 代码片段架构参考

本演练介绍如何完成以下任务:

  1. 创建并注册特定语言的代码片段。

  2. “插入代码段 ”命令添加到快捷菜单。

  3. 实现代码段扩展。

    本演练基于 Walkthrough: Display 语句完成

创建和注册代码片段

通常,代码片段与已注册的语言服务相关联。 但是,无需实现注册 LanguageService 代码片段。 相反,只需在代码段索引文件中指定 GUID,然后在添加到项目时 ProvideLanguageCodeExpansionAttribute 使用相同的 GUID。

以下步骤演示如何创建代码片段并将其与特定的 GUID 相关联。

  1. 创建以下目录结构:

    %InstallDir%\TestSnippets\Snippets\1033\

    其中 %InstallDir% 是 Visual Studio 安装文件夹。 (虽然此路径通常用于安装代码片段,但可以指定任何路径。

  2. 在 \1033\ 文件夹中,创建一个 .xml 文件并将其命名为 TestSnippets.xml。 (尽管此名称通常用于代码段索引文件,但只要具有 .xml 文件扩展名,就可以指定任何名称。添加以下文本,然后删除占位符 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. 在代码片段文件夹中创建文件,将其命名为 测试.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 内容类型,并将 Project 设置为项目的名称。

  5. 选择“完成测试”项目,然后在属性窗口将“生成 Pkgdef 文件设置为 true。 保存项目。

  6. 向项目添加静态 SnippetUtilities 类。

    static class SnippetUtilities
    
  7. 在 SnippetUtilities 类中,定义 GUID 并为其提供在 SnippetsIndex.xml 文件中使用的值。

    internal const string LanguageServiceGuidStr = "00000000-0000-0000-0000-00000000";
    
  8. 将类 ProvideLanguageCodeExpansionAttribute 添加到该 TestCompletionHandler 类。 可以将此属性添加到项目中的任何公共或内部(非静态)类。 (可能需要为 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激活 Insert Snippet 命令。 启用该命令之前,检查此方法在自动化函数中未调用,因为单击“插入代码段”命令时,它将显示代码片段选取器用户界面(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 接口。 从当前编辑器类转换为旧代码时,请记住,旧接口使用行号和列号的组合来指定文本缓冲区中的位置,但当前类使用一个索引。 因此,如果缓冲区每个行都有 10 个字符(加上一个换行符),则第三行的第四个字符在当前实现中的位置为 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 代码之后,添加以下代码来处理代码段导航(当用户在插入代码段后按 TabShift+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 。 在此实现中,感兴趣的 EndExpansion 唯一方法是和 OnItemChosen。 其他方法应只返回 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. 打开文本文件并键入一些文本。

  3. 右键单击文本中的某个位置,然后单击“ 插入代码段”。

  4. 代码片段选取器 UI 应显示一个显示 “测试替换”字段的弹出窗口。 双击弹出窗口。

    应插入以下代码片段。

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

    不要按 EnterEsc

  5. Tab 和 Shift+Tab 在“first”和“second”之间切换。

  6. EnterEsc 接受插入。

  7. 在文本的其他部分中,键入“test”,然后按 Tab。由于“test”是代码片段快捷方式,因此应再次插入代码片段。