演练:实现代码片段
可以创建代码片段并将其包含在编辑器扩展中,以便扩展的用户可以将它们添加到自己的代码中。
代码片段是可以合并到文件中的代码片段或其他文本。 若要查看已注册特定编程语言的所有代码段,请在 “工具” 菜单上,单击“ 代码段管理器”。 若要在文件中插入代码段,请右键单击所需代码段的位置,单击“插入代码段”或 “环绕”,找到所需的代码段,然后双击它。 按 Tab 或 Shift+Tab 可修改代码片段的相关部分,然后按 Enter 或 Esc 接受它。 有关详细信息,请参阅 代码片段。
代码片段包含在扩展名为 .snippet* 的 XML 文件中。 代码片段可以包含插入代码段后突出显示的字段,以便用户可以查找和更改它们。 代码段文件还提供代码片段管理器的信息,以便它可以在正确的类别中显示代码段名称。 有关代码段架构的信息,请参阅 代码片段架构参考。
本演练介绍如何完成以下任务:
创建并注册特定语言的代码片段。
将 “插入代码段 ”命令添加到快捷菜单。
实现代码段扩展。
本演练基于 Walkthrough: Display 语句完成。
创建和注册代码片段
通常,代码片段与已注册的语言服务相关联。 但是,无需实现注册 LanguageService 代码片段。 相反,只需在代码段索引文件中指定 GUID,然后在添加到项目时 ProvideLanguageCodeExpansionAttribute 使用相同的 GUID。
以下步骤演示如何创建代码片段并将其与特定的 GUID 相关联。
创建以下目录结构:
%InstallDir%\TestSnippets\Snippets\1033\
其中 %InstallDir% 是 Visual Studio 安装文件夹。 (虽然此路径通常用于安装代码片段,但可以指定任何路径。
在 \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>
在代码片段文件夹中创建文件,将其命名为 测试
.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 注册代码片段
打开 CompletionTest 项目。 有关如何创建此项目的信息,请参阅 演练:显示语句完成。
在项目中,添加对以下程序集的引用:
Microsoft.VisualStudio.TextManager.Interop
Microsoft.VisualStudio.TextManager.Interop.8.0
microsoft.msxml
在项目中,打开 source.extension.vsixmanifest 文件。
确保“ 资产 ”选项卡包含 VsPackage 内容类型,并将 Project 设置为项目的名称。
选择“完成测试”项目,然后在属性窗口将“生成 Pkgdef 文件”设置为 true。 保存项目。
向项目添加静态
SnippetUtilities
类。在 SnippetUtilities 类中,定义 GUID 并为其提供在 SnippetsIndex.xml 文件中使用的值。
将类 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
生成并运行该项目。 在运行项目时启动的 Visual Studio 实验实例中,刚刚注册的代码片段应显示在 TestSnippets 语言下的代码片段管理器中。
将“插入代码段”命令添加到快捷菜单
文本文件的快捷菜单上不包括“插入代码段”命令。 因此,必须启用该命令。
将“插入代码段”命令添加到快捷菜单
打开
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); }
生成并运行该项目。 在实验实例中,打开扩展名 为 .zzz 的文件,然后右键单击该文件中的任意位置。 “ 插入代码段 ”命令应显示在快捷菜单上。
在代码片段选取器 UI 中实现代码段扩展
本部分演示如何实现代码片段扩展,以便在快捷菜单上单击“插入代码段”时 显示代码片段 选取器 UI。 当用户键入代码片段快捷方式,然后按 Tab 时,也会扩展代码片段。
若要显示代码片段选取器 UI 并启用导航和插入后代码段接受,请使用 Exec 该方法。 插入本身由 OnItemChosen 方法处理。
代码片段扩展的实现使用旧 Microsoft.VisualStudio.TextManager.Interop 接口。 从当前编辑器类转换为旧代码时,请记住,旧接口使用行号和列号的组合来指定文本缓冲区中的位置,但当前类使用一个索引。 因此,如果缓冲区每个行都有 10 个字符(加上一个换行符),则第三行的第四个字符在当前实现中的位置为 27,但在旧实现中位于第 2 行,位置 3。
实现代码段扩展
若要包含
TestCompletionCommandHandler
该类的文件,请添加以下using
指令。TestCompletionCommandHandler
使类实现IVsExpansionClient接口。在类中
TestCompletionCommandHandlerProvider
,导入 .ITextStructureNavigatorSelectorService为代码扩展接口和 IVsTextView.
在类的
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); }
若要在用户单击“插入代码段” 命令时显示代码片段 选取器,请将以下代码添加到 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; }
如果代码片段包含可以导航的字段,则扩展会话将保持打开状态,直到显式接受扩展;如果代码片段没有字段,会话将关闭,并按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; } } }
若要在用户键入相应的快捷方式并按 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; }
实现接口的方法 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; }
实现 OnItemChosen 方法。 实际插入扩展的帮助程序方法将在后面的步骤中介绍。 提供 TextSpan 行和列信息,可从中 IVsTextView获取这些信息。
以下私有方法基于快捷方式或标题和路径插入代码片段。 然后,它使用代码片段调用 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; }
生成和测试代码片段扩展
可以测试代码段扩展是否在项目中有效。
生成解决方案。 在调试器中运行此项目时,将启动 Visual Studio 的第二个实例。
打开文本文件并键入一些文本。
右键单击文本中的某个位置,然后单击“ 插入代码段”。
代码片段选取器 UI 应显示一个显示 “测试替换”字段的弹出窗口。 双击弹出窗口。
应插入以下代码片段。
MessageBox.Show("first"); MessageBox.Show("second");
不要按 Enter 或 Esc。
按 Tab 和 Shift+Tab 在“first”和“second”之间切换。
按 Enter 或 Esc 接受插入。
在文本的其他部分中,键入“test”,然后按 Tab。由于“test”是代码片段快捷方式,因此应再次插入代码片段。