Support for Code Snippets (Managed Package Framework)
A code snippet is a piece of code that is inserted into the source file. The snippet itself is an XML-based template that describes fields which specify default values. These fields are highlighted after the snippet is inserted and can have different values depending on the context in which the snippet is inserted. Immediately after the snippet is inserted, the language service can format the snippet.
Once the snippet is inserted, a special edit mode is enabled that allows the fields of the snippet to be navigated easily by the user typing the Tab key. In addition, the fields can support IntelliSense-style drop-down menus to select one of multiple values. The user commits the snippet to the source file by typing either the Enter or ESC key. To learn more about snippets, please see Creating and Using IntelliSense Code Snippets.
Implementation
The managed package framework (MPF) provides complete support for code snippets with the exception of what are called expansion functions. An expansion function is a named function that is embedded in a snippet template and returns one or more values to be placed in a field. The values are returned by the language service itself through an ExpansionFunction object. The ExpansionFunction object must be implemented by the language service to support expansion functions.
The MPF supports all other snippet functionality, from reading the template to inserting the snippet and enabling the special edit mode. Support is managed through the ExpansionProvider class.
When the Source class is instantiated, the CreateExpansionProvider method in the LanguageService class is called to obtain an ExpansionProvider object (note that the base LanguageService class always returns a new ExpansionProvider object for each Source object).
Enabling Support for Code Snippets
To enable support for code snippets, you must provide or install the snippets and you must provide the means for the user to insert those snippets. There are three steps to enabling support for code snippets:
Installing the Snippet Files.
Installing the Registry Entries.
Invoking the ExpansionProvider object.
These are covered in the following sections.
Installing the Snippet Files
All snippets for a language are stored as templates in XML files, typically one snippet template per file. For details on the XML schema used for code snippet templates, see Code Snippets Schema Reference. Each snippet template is identified with a language ID. This language ID is specified in the registry and is put into the Language attribute of the <Code> tag in the template.
There are typically two locations where your language's snippet template files are stored: where your language was installed and in the user's folder. These locations are added to the registry so that the Code Snippets Manager of Visual Studio can find your snippets. The user's folder is where snippets created by the user are stored.
The typical folder layout for the installed snippet template files looks like this: [InstallRoot]\[TestLanguage]\Snippets\[LCID]\Snippets.
[InstallRoot] is the folder your language is installed in.
[TestLanguage] is the name of your language as a folder name.
[LCID] is the locale ID. This is how localized versions of your snippets are stored. For example, the locale ID for English is 1033, so [LCID] is replaced by 1033.
One additional file must be supplied and that is an index file, typically called SnippetsIndex.xml or ExpansionsIndex.xml (you can use any valid filename ending in .xml). This file is typically stored in the [InstallRoot]\[TestLanguage] folder and specifies the exact location of the snippets folder as well as the language ID and GUID of the language service that uses the snippets. The exact path of the index file is put into the registry as described later in "Installing the Registry Entries". Here is an example of a SnippetsIndex.xml file:
<?xml version="1.0" encoding="utf-8" ?>
<SnippetCollection>
<Language Lang="Testlanguage" Guid="{b614a40a-80d9-4fac-a6ad-fc2868fff7cd}">
<SnippetDir>
<OnOff>On</OnOff>
<Installed>true</Installed>
<Locale>1033</Locale>
<DirPath>%InstallRoot%\TestLanguage\Snippets\%LCID%\Snippets\</DirPath>
<LocalizedName>Snippets</LocalizedName>
</SnippetDir>
</Language>
</SnippetCollection>
Note the XML <Language> tag that specifies the language ID (the Lang attribute) and the language service's GUID.
Visual Studio replaces %InstallRoot% with the installed location of Visual Studio; for example, C:\Program Files\Microsoft Visual Studio 8\. This example assumes you have installed your language service in that folder. The %LCID% is replaced with the user's current locale ID. Multiple <SnippetDir> tags can be added, one each for different directories and for different locales. In addition, a snippet folder can contain subfolders, each of which is identified in the index file with the <SnippetSubDir> tag that is embedded in a <SnippetDir> tag.
Users can also create their own snippets for your language. These are typically stored in the user's settings folder, for example [TestDocs]\Code Snippets\[TestLanguage]\Test Code Snippets, where [TestDocs] is the location of the user's settings folder for Visual Studio.
The following substitution elements can be placed in the path stored in the <DirPath> tag in the index file.
Element |
Description |
---|---|
%LCID% |
Locale ID. |
%InstallRoot% |
Root installation folder for Visual Studio, for example, C:\Program Files\Microsoft Visual Studio 8. |
%ProjDir% |
Folder containing the current project. |
%ProjItem% |
Folder containing the current project item. |
%TestDocs% |
Folder in the user's settings folder, for example, C:\Documents and Settings\[username]\My Documents\Visual Studio\8. |
Installing the Registry Entries
A number of registry entries must be made for your language service to indicate support for code snippets and to provide the location of the snippets index file as well as a default set of paths to search for snippets. These entries can be made using the ProvideLanguageCodeExpansionAttribute attribute that is applied to your VSPackage (see Registering a Language Service (Managed Package Framework) for details).
Here is an example layout of the registry keys and entries.
Note
All of these registry entries are required except for the ShowRoots entry and the Paths sub-key.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\[Version]\
Languages\
CodeExpansions\
[Language Name]\
(Default) = rg_sz: [Language Service GUID]
DisplayName = rg_sz: [Resource ID]
IndexPath = rg_sz: [Path to snippet index file]
LangStringID = rg_sz: [Language ID String]
Package = rg_sz: [Language Package GUID]
ShowRoots = rg_dword: 0x00000000
Paths\
(Default) = rg_sz:
[Language Name] = rg_sz: [List of snippet paths]
Registry Term |
Description |
---|---|
[Version] |
Version of Visual Studio; for example, 8.0 or 8.0Exp. |
[Language Name] |
Name of the language. This is the same name the language is registered under in the Language Services key (see Registering a Language Service (Managed Package Framework)). |
[Language Service GUID] |
GUID of the language service. |
[Resource ID] |
A decimal number string specifying a resource ID that in turn specifies the name to use in the Code Snippets Browser dialog box. If the resource cannot be loaded, the [Language Name] registry key name is used. |
[Path to snippet index file] |
The full path to the snippets index file. The same substitution values used in the snippets index file can be used here. |
[Language ID String] |
A string that is used to identify the contents of the snippets index file (the Lang attribute in the <Language> tag) and the individual snippets (the Language attribute in the <Code> tag). If this value does not match the value in the snippets index file or the snippet template files, then those files are not loaded. |
[Language Package GUID] |
GUID of the package that proffers the language service. |
[List of snippet paths] |
A semi-colon-delimited list of paths that are searched for snippet index files. The same substitution values used in the snippets index file can be used here. |
Invoking the ExpansionProvider Object
The language service controls the insertion of any code snippet as well as how that insertion is invoked. There are two ways the language service can invoke insertion of a code snippet (see the "Implementing Support for Code Snippets" section for details on how the following invocation methods are implemented):
- Use a menu command to initiate a call to the DisplayExpansionBrowser method in the ExpansionProvider class, which displays a list of all available snippets from which to choose.
Note
Beginning with Visual Studio 2008 SDK, use XML Command Table (.vsct) files instead of command table configuration (.ctc) files to define how menus and commands appear in your VSPackages. For more information, see Visual Studio Command Table (.Vsct) Files.
The menu command must be added to your CTC file and installed on an appropriate menu.
The snippets that are displayed are those for your language and are the snippets installed with your language. The snippet is inserted when the user makes a selection. The following methods in the ExpansionProvider class are called by Visual Studio in the given order during the process of inserting the snippet:
After the OnAfterInsertion method is called, the snippet has been inserted and the ExpansionProvider object is in a special edit mode used for modifying a snippet that has just been inserted.
Add the shortcut name of a code snippet to an IntelliSense word completion list that is shown for the user types. The language service uses the shortcut name to insert the associated snippet.
When the language service gets the shortcut name, it calls the FindExpansionByShortcut method in the ExpansionProvider class to obtain the filename and code snippet title. The language service then calls the InsertNamedExpansion method in the ExpansionProvider class to insert the code snippet. The following methods are called by Visual Studio in the given order in the ExpansionProvider class during the process of inserting the snippet:
Add the shortcut name of a code snippet to an IntelliSense word completion list that is shown for the user types. The language service uses the shortcut name to insert the associated snippet.
When the language service gets the shortcut name, it calls the FindExpansionByShortcut method in the ExpansionProvider class to obtain the filename and code snippet title. The language service then calls the InsertNamedExpansion method in the ExpansionProvider class to insert the code snippet. The following methods are called by Visual Studio in the given order in the ExpansionProvider class during the process of inserting the snippet:
Implementing Support for Code Snippets
There are two ways to insert a code snippet: through a menu selection or through a shortcut selection from a completion list. Here are two examples of these methods, one for "Menu Selection" and the other for "Shortcut Selection".
Menu Selection
The MPF provides complete support for using the Code Snippet Browser dialog box. All you need to do is add a menu command and then call the DisplayExpansionBrowser method in the ExpansionProvider interface in response to that menu command.
Add a button to your CTC file (typically pkgcmd.ctc) that represents the menu command. You must also add a definition for the command ID (in this example, the command ID is called InvokeCodeSnippetsBrowser) to the command ID header file, typically called PkgCmdID.h (which is used by the CTC compiler. You also need to add the command ID to the PkgCmdID.cs so your C# classes can refer to the command ID by name). This example puts your menu option at the top of the Tools menu.
PkgCmdID.h /////////////////////////////////////////////////////////////////////////////// // Command IDs #define InvokeCodeSnippetsBrowser 1 ------------------------------------------------------------------ PkgCmdID.cs namespace TestLanguagePackage { class PkgCmdIDList { public const int InvokeCodeSnippetsBrowser = 1; }; } ------------------------------------------------------------------ PkgCmd.ctc CMDS_SECTION guidTestLanguagePackagePkg NEWGROUPS_BEGIN guidTestLanguagePackageCmdSet:TestMenuGroup, // New Group guidSHLMainMenu:IDM_VS_MENU_TOOLS, // Parent group 0x0600; // Priority NEWGROUPS_END BUTTONS_BEGIN guidTestLanguagePackageCmdSet:InvokeCodeSnippetsBrowser, // Command guidTestLanguagePackageCmdSet:TestMenuGroup, // Parent group 0x10, // Priority OI_NOID, // Icon (no icon here) BUTTON, // Type (always BUTTON) TextOnly | DEFAULTDISABLED, // Flags "&Insert Snippet..."; // Text BUTTONS_END CMDS_END
Derive a class from the ViewFilter class and override the QueryCommandStatus method to indicate support for the new menu command. This example always enables the menu command.
using Microsoft.VisualStudio.Package; namespace TestLanguagePackage { class TestViewFilter : ViewFilter { public TestViewFilter(CodeWindowManager mgr, IVsTextView view) : base(mgr, view) { } protected override int QueryCommandStatus(ref Guid guidCmdGroup, uint nCmdId) { int hr = base.QueryCommandStatus(ref guidCmdGroup, nCmdId); // If the base class did not recognize the command then // see if we can handle the command. if (hr == (int)Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_UNKNOWNGROUP) { if (guidCmdGroup == GuidList.guidTestLanguagePackageCmdSet) { if (nCmdId == PkgCmdIDList.InvokeCodeSnippetsBrowser) { hr = (int)(OLECMDF.OLECMDF_SUPPORTED | OLECMDF.OLECMDF_ENABLED); } } } return hr; } } }
Override the HandlePreExec method in the ViewFilter class to obtain the ExpansionProvider object and call the DisplayExpansionBrowser method on that object.
using Microsoft.VisualStudio.Package; namespace TestLanguagePackage { class TestViewFilter : ViewFilter { public override bool HandlePreExec(ref Guid guidCmdGroup, uint nCmdId, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { if (base.HandlePreExec(ref guidCmdGroup, nCmdId, nCmdexecopt, pvaIn, pvaOut)) { // Base class handled the command. Do nothing more here. return true; } if (guidCmdGroup == GuidList.guidTestLanguagePackageCmdSet) { if (nCmdId == PkgCmdIDList.InvokeCodeSnippetsBrowser) { ExpansionProvider ep = this.GetExpansionProvider(); if (this.TextView != null && ep != null) { bool bDisplayed = ep.DisplayExpansionBrowser( this.TextView, "TestLanguagePackage Snippet:", null, false, null, false); } return true; // Handled the command. } } return false; // Did not handle the command. } } }
Shortcut Selection
Implementation of a shortcut selection from a completion list is much more involved than the menu selection. You must first add snippet shortcuts to the IntelliSense word completion list. Then you must detect when a snippet shortcut name has been inserted as a result of completion. Finally, you must obtain the snippet title and path using the shortcut name and pass that information to the InsertNamedExpansion method on the ExpansionProvider method.
How you add a snippet shortcut to the Declarations object contained in your version of the AuthoringScope class is entirely up to you. Just make sure you can identify the shortcut as a snippet name. For an example, see Walkthrough: Getting a List of Installed Code Snippets (Managed Package Framework).
One place where you can detect the insertion of the code snippet shortcut is in the OnAutoComplete method of the Declarations class. Just be aware that the snippet name has already been inserted into the source file and needs to be removed. The InsertNamedExpansion method takes a span that describes the point of insertion for the snippet; if the span refers to the entire snippet name in the source file, that name is replaced by the snippet.
Here is a version of a Declarations class that handles snippet insertion given a shortcut name. Other methods in the Declarations class have been omitted for clarity. Note that the constructor of this class takes a LanguageService object, not a typical argument for the base Declarations class. This must somehow be passed in from your version of the AuthoringScope object (for example, your implementation of the AuthoringScope class might take the LanguageService object in its constructor and pass that object on to your TestDeclarations class constructor).
[C#]
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;
namespace TestLanguagePackage
{
internal class TestDeclarations : Declarations
{
private ArrayList declarations;
private LanguageService languageService;
private TextSpan commitSpan;
public TestDeclarations(LanguageService langService)
: base()
{
languageService = langService;
declarations = new ArrayList();
}
// This method is used to add declarations to the internal list.
public void AddDeclaration(TestDeclaration declaration)
{
declarations.Add(declaration);
}
// This method is called to get the string to commit to the source buffer.
// Note that the initial extent is only what the user has typed so far.
public override string OnCommit(IVsTextView textView,
string textSoFar,
char commitCharacter,
int index,
ref TextSpan initialExtent)
{
// We intercept this call only to get the initial extent
// of what was committed to the source buffer.
commitSpan = initialExtent;
return base.OnCommit(textView,
textSoFar,
commitCharacter,
index,
ref initialExtent);
}
// This method is called after the string has been committed to the source buffer.
public override char OnAutoComplete(IVsTextView textView,
string committedText,
char commitCharacter,
int index)
{
TestDeclaration item = declarations[index] as TestDeclaration;
if (item != null)
{
// In this example, TestDeclaration identifies types with a string.
// You can choose a different approach.
if (item.Type == "snippet")
{
Source src = languageService.GetSource(textView);
if (src != null)
{
ExpansionProvider ep = src.GetExpansionProvider();
if (ep != null)
{
string title;
string path;
int commitLength = commitSpan.iEndIndex - commitSpan.iStartIndex;
if (commitLength < committedText.Length)
{
// Replace everything that was inserted
// so calculate the span of the full
// insertion, taking into account what
// was inserted when the commitSpan
// was obtained in the first place.
commitSpan.iEndIndex += (committedText.Length - commitLength);
}
if (ep.FindExpansionByShortcut(textView,
committedText,
commitSpan,
true,
out title,
out path))
{
ep.InsertNamedExpansion(textView,
title,
path,
commitSpan,
false);
}
}
}
}
}
return '\0';
}
}
}
Note
To learn how to programmatically obtain a list of all installed code snippets for your language service, see Walkthrough: Getting a List of Installed Code Snippets (Managed Package Framework).
Implementing the ExpansionFunction Class
An expansion function is a named function that is embedded in a snippet template and returns one or more values to be placed in a field. In order to support expansion functions in your language service, you must derive a class from the ExpansionFunction class and implement the GetCurrentValue method. You must then override the CreateExpansionFunction method in the LanguageService class to return a new instantiation of your version of the ExpansionFunction class for each expansion function you support. If you support a list of possible values from an expansion function, you must also override the GetIntellisenseList method in the ExpansionFunction class to return a list of those values.
Warning
An expansion function that takes arguments or needs to access other fields should not be associated with an editable field, as the expansion provider might not be fully initialized by the time the expansion function is called. As a result, the expansion function is not able to obtain the value of its arguments or any other field.
Example
Here is an example of how a simple expansion function called GetName might be implemented. This expansion function appends a number to a base class name each time the expansion function is instantiated (which corresponds to each time the associated code snippet is inserted).
using Microsoft.VisualStudio.Package;
namespace TestLanguagePackage
{
public class TestLanguageService : LanguageService
{
private int classNameCounter = 0;
public override ExpansionFunction CreateExpansionFunction(
ExpansionProvider provider,
string functionName)
{
ExpansionFunction function = null;
if (functionName == "GetName")
{
++classNameCounter;
function = new TestGetNameExpansionFunction(provider, classNameCounter);
}
return function;
}
}
internal class TestGetNameExpansionFunction : ExpansionFunction
{
private int nameCount;
TestGetNameExpansionFunction(ExpansionProvider provider, int counter)
: base(provider)
{
nameCount = counter;
}
public override string GetCurrentValue()
{
string name = "TestClass";
name += nameCount.ToString();
return name;
}
}
}
See Also
Tasks
Walkthrough: Getting a List of Installed Code Snippets (Managed Package Framework)
Concepts
Registering a Language Service (Managed Package Framework)
Reference
Creating and Using IntelliSense Code Snippets
Other Resources
Language Service Features (Managed Package Framework)
Change History
Date |
History |
Reason |
---|---|---|
July 2008 |
Rewrote and refactored project. |
Content bug fix. |