Walkthrough: Implementing Code Snippets
You can create code snippets and include them in an editor extension so that users of the extension can add them to their own code.
A code snippet is a fragment of code or other text that can be incorporated in a file. To view all snippets that have been registered for particular programming languages, on the Tools menu, click Code Snippet Manager. To insert a snippet in a file, right-click where you want the snippet, click Insert Snippet or Surround With, locate the snippet you want, and then double-click it. Press TAB or SHIFT+TAB to modify the relevant parts of the snippet and then press ENTER or ESC to accept it. For more information, see Code Snippets.
A code snippet is contained in an XML file that has the .snippet file name extension. A snippet can contain fields that are highlighted after the snippet is inserted so that the user can find and change them. A snippet file also provides information for the Code Snippet Manager so that it can display the snippet name in the correct category. For information about the snippet schema, see Code Snippets Schema Reference.
This walkthrough teaches how to accomplish these tasks:
Create and register code snippets for a specific language.
Add the Insert Snippet command to a shortcut menu.
Implement snippet expansion.
This walkthrough is based on Walkthrough: Displaying Statement Completion.
Prerequisites
To complete this walkthrough, you must install the Visual Studio 2012 SDK. For information about the Visual Studio SDK, and to download it, see Visual Studio Extensibility Developer Center on the MSDN Web site.
Creating and Registering Code Snippets
Typically, code snippets are associated with a registered language service. However, you do not have to implement a LanguageService to register code snippets. Instead, just specify a GUID in the snippet index file and then use the same GUID in the ProvideLanguageCodeExpansionAttribute that you add to your project.
The following steps demonstrate how to create code snippets and associate them with a specific GUID.
To create code snippets
Create the following directory structure:
%InstallDir%\TestSnippets\Snippets\1033\
where %InstallDir% is the Visual Studio installation folder. (Although this path is typically used to install code snippets, you can specify any path.)
In the \1033\ folder, create an .xml file and name it SnippetIndex.xml. (Although this name is typically used for a snippet index file, you can specify any name as long as it has an .xml file name extension.) Add the following text, and then delete the placeholder GUID and add your own.
<?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>
Create a file in the snippet folder, name it test.snippet, and then add the following text:
<?xml version="1.0" encoding="utf-8" ?> <CodeSnippets xmlns="https://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>
The following steps show how to register the code snippets.
To register code snippets for a specific GUID
Open the CompletionTest project. For information about how to create this project, see Walkthrough: Displaying Statement Completion.
In the project, add references to the following assemblies:
Microsoft.VisualStudio.TextManager.Interop
Microsoft.VisualStudio.TextManager.Interop.8.0
microsoft.msxml
In the project, open the source.extension.vsixmanifest file.
In the Content section, click Add Content, select the VS Package content type, select the Project option, and select TestCompletion in the dropdown list.
The following line should appear in the file:
TestCompletion;PkgdefProjectOutputGroup|
Save the source.extension.vsixmanifest file and close it.
Add a static SnippetUtilities class to the project.
Module SnippetUtilities
static class SnippetUtilities
In the SnippetUtilities class, define a GUID and give it the value that you used in the SnippetsIndex.xml file.
Friend Const LanguageServiceGuidStr As String = "00000000-0000-0000-0000-00000000"
internal const string LanguageServiceGuidStr = "00000000-0000-0000-0000-00000000";
Add the ProvideLanguageCodeExpansionAttribute to the TestCompletionHandler class. This attribute can be added to any public or internal (non-static) class in the project. (You may have to add a using statement for the Microsoft.VisualStudio.Shell namespace.)
<ProvideLanguageCodeExpansion( SnippetUtilities.LanguageServiceGuidStr, "TestSnippets", 0, "TestSnippets", "%InstallRoot%\TestSnippets\Snippets\%LCID%\SnippetsIndex.xml", SearchPaths:="%InstallRoot%\TestSnippets\Snippets\%LCID%\", ForceCreateDirs:="%InstallRoot%\TestSnippets\Snippets\%LCID%\")> Friend Class TestCompletionCommandHandler Implements IOleCommandTarget
[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%\SnippetsIndex.xml", //the path of the index file SearchPaths = @"%InstallRoot%\TestSnippets\Snippets\%LCID%\", ForceCreateDirs = @"%InstallRoot%\TestSnippets\Snippets\%LCID%\")] internal class TestCompletionCommandHandler : IOleCommandTarget
Build and run the project. In the experimental instance of Visual Studio that starts when the project is run, the snippet you just registered should be displayed in the Code Snippets Manager under the TestSnippets language.
Adding the Insert Snippet Command to the Shortcut Menu
The Insert Snippet command is not included on the shortcut menu for a text file. Therefore, you must enable the command.
To add the Insert Snippet command to the shortcut menu
Open the TestCompletionCommandHandler class file.
Because this class implements IOleCommandTarget, you can activate the Insert Snippet command in the QueryStatus method. Before you enable the command, check that this method is not being called inside an automation function because when the Insert Snippet command is clicked, it will display the snippet picker user interface (UI).
Public Function QueryStatus(ByRef pguidCmdGroup As Guid, ByVal cCmds As UInteger, ByVal prgCmds As OLECMD(), ByVal pCmdText As IntPtr) As Integer Implements IOleCommandTarget.QueryStatus If Not VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider) Then If pguidCmdGroup = VSConstants.VSStd2K AndAlso cCmds > 0 Then ' make the Insert Snippet command appear on the context menu If CUInt(prgCmds(0).cmdID) = CUInt(VSConstants.VSStd2KCmdID.INSERTSNIPPET) Then prgCmds(0).cmdf = CInt(Constants.MSOCMDF_ENABLED) Or CInt(Constants.MSOCMDF_SUPPORTED) Return VSConstants.S_OK End If End If End If Return m_nextCommandHandler.QueryStatus(pguidCmdGroup, cCmds, prgCmds, pCmdText) End Function
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); }
Build and run the project. In the experimental instance, open a file that has the .zzz file name extension and then right-click anywhere in it. The Insert Snippet command should appear on the shortcut menu.
Implementing Snippet Expansion in the Snippet Picker UI
This section shows how to implement code snippet expansion so that the snippet picker UI is displayed when Insert Snippet is clicked on the shortcut menu. A code snippet is also expanded when a user types the code-snippet shortcut and then presses TAB.
To display the snippet picker UI and to enable navigation and post-insertion snippet acceptance, use the Exec method. The insertion itself is handled by the OnItemChosen method.
The implementation of code snippet expansion uses legacy Microsoft.VisualStudio.TextManager.Interop interfaces. When you translate from the current editor classes to the legacy code, remember that the legacy interfaces use a combination of line numbers and column numbers to specify locations in a text buffer, but the current classes use one index. Therefore, if a buffer has three lines each of which has ten characters (plus a newline, which counts as 1 character), the fourth character on the third line is at position 27 in the current implementation, but it is at line 2, position 3 in the old implementation.
To implement snippet expansion
To the file that contains the TestCompletionCommandHandler class, add the following using statements.
Imports Microsoft.VisualStudio.Text.Operations Imports MSXML
using Microsoft.VisualStudio.Text.Operations; using MSXML; using System.ComponentModel.Composition;
Make the TestCompletionCommandHandler class implement the IVsExpansionClient interface.
<ProvideLanguageCodeExpansion( SnippetUtilities.LanguageServiceGuidStr, "TestSnippets", 0, "TestSnippets", "%InstallRoot%\TestSnippets\Snippets\%LCID%\SnippetsIndex.xml", SearchPaths:="%InstallRoot%\TestSnippets\Snippets\%LCID%\", ForceCreateDirs:="%InstallRoot%\TestSnippets\Snippets\%LCID%\")> Friend Class TestCompletionCommandHandler Implements IOleCommandTarget Implements IVsExpansionClient
internal class TestCompletionCommandHandler : IOleCommandTarget, IVsExpansionClient
In the TestCompletionCommandHandlerProvider class, import the ITextStructureNavigatorSelectorService.
<Import()> Friend Property NavigatorService As ITextStructureNavigatorSelectorService
[Import] internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
Add some private fields for the code expansion interfaces and the IVsTextView.
Dim m_vsTextView As IVsTextView Dim m_exManager As IVsExpansionManager Dim m_exSession As IVsExpansionSession
IVsTextView m_vsTextView; IVsExpansionManager m_exManager; IVsExpansionSession m_exSession;
In the constructor of the TestCompletionCommandHandler class, set the following fields.
Friend Sub New(ByVal textViewAdapter As IVsTextView, ByVal textView As ITextView, ByVal provider As TestCompletionHandlerProvider) Me.m_textView = textView Me.m_provider = provider Me.m_vsTextView = textViewAdapter Dim textManager As IVsTextManager2 = DirectCast(m_provider.ServiceProvider.GetService(GetType(SVsTextManager)), IVsTextManager2) textManager.GetExpansionManager(m_exManager) m_exSession = Nothing 'add the command to the command chain textViewAdapter.AddCommandFilter(Me, m_nextCommandHandler) End Sub
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); }
To display the snippet picker when the user clicks the Insert Snippet command, add the following code to the Exec method. (To make this explanation more readable, the Exec() code that is used for statement completion is not shown; instead, blocks of code are added to the existing method.) Add the following block of code after the code that checks for a character.
'code previously written for Exec If pguidCmdGroup = VSConstants.VSStd2K AndAlso nCmdID = CUInt(VSConstants.VSStd2KCmdID.TYPECHAR) Then typedChar = ChrW(CUShort(Marshal.GetObjectForNativeVariant(pvaIn))) End If 'the snippet picker code starts here If nCmdID = CUInt(VSConstants.VSStd2KCmdID.INSERTSNIPPET) Then Dim textManager As IVsTextManager2 = DirectCast(m_provider.ServiceProvider.GetService(GetType(SVsTextManager)), IVsTextManager2) textManager.GetExpansionManager(m_exManager) m_exManager.InvokeInsertionUI( m_vsTextView, Me, New Guid(SnippetUtilities.LanguageServiceGuidStr), Nothing, 0, 0, Nothing, 0, 0, "TestSnippets", String.Empty) Return VSConstants.S_OK End If
//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; }
If a snippet has fields that can be navigated, the expansion session is kept open until the expansion is explicitly accepted; if the snippet has no fields, the session is closed and is returned as null by the InvokeInsertionUI method. In the Exec method, after the snippet picker UI code that you added in the previous step, add the following code to handle snippet navigation (when the user presses TAB or SHIFT+TAB after snippet insertion).
'the expansion insertion is handled in OnItemChosen 'if the expansion session is still active, handle tab/backtab/return/cancel If m_exSession IsNot Nothing Then If nCmdID = CUInt(VSConstants.VSStd2KCmdID.BACKTAB) Then m_exSession.GoToPreviousExpansionField() Return VSConstants.S_OK ElseIf nCmdID = CUInt(VSConstants.VSStd2KCmdID.TAB) Then m_exSession.GoToNextExpansionField(0) 'false to support cycling through all the fields Return VSConstants.S_OK ElseIf nCmdID = CUInt(VSConstants.VSStd2KCmdID.[RETURN]) OrElse nCmdID = CUInt(VSConstants.VSStd2KCmdID.CANCEL) Then If m_exSession.EndCurrentExpansion(0) = VSConstants.S_OK Then m_exSession = Nothing Return VSConstants.S_OK End If End If End If
//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; } } }
To insert the code snippet when the user types the corresponding shortcut and then presses TAB, add code to the Exec method. The private method that inserts the snippet will be shown in a later step. Add the following code after the navigation code that you added in the previous step.
'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 Is Nothing AndAlso m_exSession Is Nothing AndAlso nCmdID = CUInt(VSConstants.VSStd2KCmdID.TAB) Then 'get the word that was just added Dim pos As CaretPosition = m_textView.Caret.Position Dim word As TextExtent = m_provider.NavigatorService.GetTextStructureNavigator(m_textView.TextBuffer).GetExtentOfWord(pos.BufferPosition - 1) Dim textString As String = word.Span.GetText() 'if it is a code snippet, insert it, otherwise carry on If InsertAnyExpansion(textString, Nothing, Nothing) Then Return VSConstants.S_OK End If End If
//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; }
Implement the methods of the IVsExpansionClient interface. In this implementation, the only methods of interest are EndExpansion and OnItemChosen. The other methods should just return S_OK.
Public Function EndExpansion() As Integer Implements IVsExpansionClient.EndExpansion m_exSession = Nothing Return VSConstants.S_OK End Function Public Function FormatSpan(ByVal pBuffer As IVsTextLines, ByVal ts As TextSpan()) As Integer Implements IVsExpansionClient.FormatSpan Return VSConstants.S_OK End Function Public Function GetExpansionFunction(ByVal xmlFunctionNode As IXMLDOMNode, ByVal bstrFieldName As String, ByRef pFunc As IVsExpansionFunction) As Integer Implements IVsExpansionClient.GetExpansionFunction pFunc = Nothing Return VSConstants.S_OK End Function Public Function IsValidKind(ByVal pBuffer As IVsTextLines, ByVal ts As TextSpan(), ByVal bstrKind As String, ByRef pfIsValidKind As Integer) As Integer Implements IVsExpansionClient.IsValidKind pfIsValidKind = 1 Return VSConstants.S_OK End Function Public Function IsValidType(ByVal pBuffer As IVsTextLines, ByVal ts() As TextSpan, ByVal rgTypes() As String, ByVal iCountTypes As Integer, ByRef pfIsValidType As Integer) As Integer Implements IVsExpansionClient.IsValidType pfIsValidType = 1 Return VSConstants.S_OK End Function Public Function OnAfterInsertion(ByVal pSession As IVsExpansionSession) As Integer Implements IVsExpansionClient.OnAfterInsertion Return VSConstants.S_OK End Function Public Function OnBeforeInsertion(ByVal pSession As IVsExpansionSession) As Integer Implements IVsExpansionClient.OnBeforeInsertion Return VSConstants.S_OK End Function Public Function PositionCaretForEditing(ByVal pBuffer As IVsTextLines, ByVal ts As TextSpan()) As Integer Implements IVsExpansionClient.PositionCaretForEditing Return VSConstants.S_OK End Function
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; }
Implement the OnItemChosen method. The helper method that actually inserts the expansions will be covered in a later step. The TextSpan provides line and column information, which you can get from the IVsTextView.
Public Function OnItemChosen(ByVal pszTitle As String, ByVal pszPath As String) As Integer Implements IVsExpansionClient.OnItemChosen InsertAnyExpansion(Nothing, pszTitle, pszPath) Return VSConstants.S_OK End Function
public int OnItemChosen(string pszTitle, string pszPath) { InsertAnyExpansion(null, pszTitle, pszPath); return VSConstants.S_OK; }
The following private method inserts a code snippet, based either on the shortcut or on the title and path. It then calls the InsertNamedExpansion method with the snippet.
Private Function InsertAnyExpansion(ByVal shortcut As String, ByVal title As String, ByVal path As String) As Boolean Dim endColumn As Integer, startLine As Integer 'get the column number from the IVsTextView, not the ITextView m_vsTextView.GetCaretPos(startLine, endColumn) Dim addSpan As New TextSpan() addSpan.iStartIndex = endColumn addSpan.iEndIndex = endColumn addSpan.iStartLine = startLine addSpan.iEndLine = startLine 'get the expansion from the shortcut If shortcut IsNot Nothing Then '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( Me, New Guid(SnippetUtilities.LanguageServiceGuidStr), shortcut, m_vsTextView, New TextSpan() {addSpan}, 0, path, title) End If If title IsNot Nothing AndAlso path IsNot Nothing Then Dim textLines As IVsTextLines = Nothing m_vsTextView.GetBuffer(textLines) Dim bufferExpansion As IVsExpansion = DirectCast(textLines, IVsExpansion) If bufferExpansion IsNot Nothing Then Dim hr As Integer = bufferExpansion.InsertNamedExpansion( title, path, addSpan, Me, New Guid(SnippetUtilities.LanguageServiceGuidStr), 0, m_exSession) If VSConstants.S_OK = hr Then Return True End If End If End If Return False End Function
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; }
Building and Testing Code Snippet Expansion
You can test whether snippet expansion works in your project.
To build and test code snippet expansion
Build the solution. When you run this project in the debugger, a second instance of Visual Studio is instantiated.
Open a text file and type some text.
Right-click somewhere in the text and then click Insert Snippet.
The snippet picker UI should appear and display the string "Test replacement fields". Double-click this string.
The following snippet should be inserted.
MessageBox.Show("first"); MessageBox.Show("second");
Do not press ENTER or ESC.
Press TAB and SHIFT+TAB to toggle between "first" and "second".
Accept the insertion by pressing either ENTER or ESC.
In a different part of the text, type "test" and then press TAB. Because "test" is the code-snippet shortcut, the snippet should be inserted again.