Create a simple extension
In Create your first extension, you learned how to use the VisualStudio.Extensibility project template to create an extension project, and learned how to build it and debug it in the experimental instance of Visual Studio.
In this tutorial, you learn how to create an extension with a simple command that does something in the Visual Studio editor. In this case, it inserts a newly generated GUID. You also see how to tell Visual Studio what file types the GUID extension is enabled for, and how to make the new command show up as a toolbar or menu item.
The completed sample for this tutorial may be found here.
The tutorial contains the following steps:
Configure the command
In this step, you'll learn about options for configuring and placing the command. The purpose of hosting the command is to expose it to the user in some way, such as adding a menu item or a command bar button.
The project template or the sample you created in the Create your first extension tutorial consists of a single C# file that includes a Command
class already. You can update that in place.
Rename the
Command1.cs
file toInsertGuidCommand.cs
, rename the classInsertGuidCommand
, update theCommandConfiguration
property.public override CommandConfiguration CommandConfiguration => new("%InsertGuidCommand.DisplayName%") { Placements = new[] { CommandPlacement.KnownPlacements.ExtensionsMenu }, };
Placements
specifies where the command should appear in the IDE. In this case, the command is placed in the Extensions menu, one of the top-level menus in Visual Studio.The argument to the
CommandConfiguration
constructor is the command's display name, which is the menu text. The display name is enclosed by%
characters because it references a string resource from.vsextension/string-resources.json
to support localization.Update
.vsextension/string-resources.json
with the display name ofInsertGuidCommand
.{ "InsertGuidCommand.DisplayName": "Insert new guid" }
Add the
Icon
property.public override CommandConfiguration CommandConfiguration => new("%InsertGuidCommand.DisplayName%") { Placements = new[] { CommandPlacement.KnownPlacements.ExtensionsMenu }, Icon = new(ImageMoniker.KnownValues.OfficeWebExtension, IconSettings.IconAndText), };
You can specify a known built-in icon, in this case
OfficeWebExtension
, or upload images for the icon as described in Add Visual Studio commands. The second argument is an enumeration that determines how the command should appear in toolbars (in addition to its place in a menu). The optionIconSettings.IconAndText
means show the icon and the display name next to each other.Add the
VisibleWhen
property, which specifies the conditions that must apply for the item to appear to the user.public override CommandConfiguration CommandConfiguration => new("%InsertGuidCommand.DisplayName%") { Placements = new[] { CommandPlacement.KnownPlacements.ExtensionsMenu }, Icon = new(ImageMoniker.KnownValues.OfficeWebExtension, IconSettings.IconAndText), VisibleWhen = ActivationConstraint.ClientContext(ClientContextKey.Shell.ActiveEditorContentType, ".+"), };
See using rule based activation constraints for more information.
Create the execution method
In this step, you implement the command's ExecuteCommandAsync
method, which defines what happens when the user chooses the menu item, or presses the item in the toolbar for your command.
Copy the following code to implement the method.
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
Requires.NotNull(context, nameof(context));
var newGuidString = Guid.NewGuid().ToString("N", CultureInfo.CurrentCulture);
using var textView = await context.GetActiveTextViewAsync(cancellationToken);
if (textView is null)
{
this.logger.TraceInformation("There was no active text view when command is executed.");
return;
}
await this.Extensibility.Editor().EditAsync(
batch =>
{
textView.Document.AsEditable(batch).Replace(textView.Selection.Extent, newGuidString);
},
cancellationToken);
}
The first line validates the arguments, then we create a new Guid
to use later.
Then, we create an ITextViewSnapshot
(the textView
object here) by calling the asynchronous method GetActiveTextViewAsync
. A cancellation token is passed in to preserve the ability to cancel the asynchronous request, but this part isn't demonstrated in this sample. If we don't get a text view successfully, we write to the log and terminate without doing anything else.
Now we're ready to call the asynchronous method that submits an edit request to Visual Studio's editor. The method we want is EditAsync
. That's a member of the EditorExtensibility
class, which allows interaction with the running Visual Studio Editor in the IDE. The Command
type, which your own InsertGuidCommand
class inherits from, has a member Extensibility
that provides access to the EditorExtensibility
object, so we can get to the EditorExtensibility
class with a call to this.Extensibility.Editor()
.
The EditAsync
method takes an Action<IEditBatch>
as a parameter. This parameter is called editorSource
,
The call to EditAsync
uses a lambda expression. To break this down a little, you could also write that call as follows:
await this.Extensibility.Editor().EditAsync(
batch =>
{
var editor = textView.Document.AsEditable(batch);
// specify the desired changes here:
editor.Replace(textView.Selection.Extent, newGuidString);
},
cancellationToken);
You could think of this call as specifying the code that you want to be run in the Visual Studio editor process. The lambda expression specifies what you want changed in the editor. The batch
is of type IEditBatch
, which implies that the lambda expression defined here makes a small set of changes that should be accomplished as a unit, rather than being interrupted by other edits by the user or language service. If the code executes too long, that can lead to unresponsiveness, so it's important to keep changes within this lambda expression limited and understand anything that could lead to delays.
Using the AsEditable
method on the document, you get a temporary editor object that you can use to specify the desired changes. Think of everything in the lambda expression as a request for Visual Studio to execute rather than as actually executing, because as described in the Use Visual Studio editor extensibility, there's a particular protocol for handling these asynchronous edit requests from extensions, and there's a possibility of the changes not being accepted, such as if the user is making changes at the same time that create a conflict.
The EditAsync
pattern can be used to modify text in general by specifying your modifications after the "specify your desired changes here" comment.