Walkthrough: Use a shell command with an editor extension
From a VSPackage, you can add features such as menu commands to the editor. This walkthrough shows how to add an adornment to a text view in the editor by invoking a menu command.
This walkthrough demonstrates the use of a VSPackage together with a Managed Extensibility Framework (MEF) component part. You must use a VSPackage to register the menu command with the Visual Studio shell. And, you can use the command to access the MEF component part.
Create an extension with a menu command
Create a VSPackage that puts a menu command named Add Adornment on the Tools menu.
Create a C# VSIX project named
MenuCommandTest
, and add a Custom Command item template name AddAdornment. For more information, see Create an extension with a menu command.A solution named MenuCommandTest opens. The MenuCommandTestPackage file has the code that creates the menu command and puts it on the Tools menu. At this point, the command just causes a message box to appear. Later steps will show how to change this to display the comment adornment.
Open the source.extension.vsixmanifest file in the VSIX Manifest Editor. The
Assets
tab should have a row for a Microsoft.VisualStudio.VsPackage named MenuCommandTest.Save and close the source.extension.vsixmanifest file.
Add a MEF extension to the command extension
In Solution Explorer, right-click the solution node, click Add, and then click New Project. In the Add New Project dialog box, click Extensibility under Visual C#, then VSIX Project. Name the project
CommentAdornmentTest
.Because this project will interact with the strong-named VSPackage assembly, you must sign the assembly. You can reuse the key file already created for the VSPackage assembly.
Open the project properties and select the Signing tab.
Select Sign the assembly.
Under Choose a strong name key file, select the Key.snk file that was generated for the MenuCommandTest assembly.
Refer to the MEF extension in the VSPackage project
Because you are adding a MEF component to the VSPackage, you must specify both kinds of assets in the manifest.
Note
For more information about MEF, see Managed Extensibility Framework (MEF).
To refer to the MEF component in the VSPackage project
In the MenuCommandTest project, open the source.extension.vsixmanifest file in the VSIX Manifest Editor.
On the Assets tab, click New.
In the Type list, choose Microsoft.VisualStudio.MefComponent.
In the Source list, choose A project in current solution.
In the Project list, choose CommentAdornmentTest.
Save and close the source.extension.vsixmanifest file.
Make sure that the MenuCommandTest project has a reference to the CommentAdornmentTest project.
In the CommentAdornmentTest project, set the project to produce an assembly. In the Solution Explorer, select the project and look in the Properties window for the Copy Build Output to OutputDirectory property, and set it to true.
Define a comment adornment
The comment adornment itself consists of an ITrackingSpan that tracks the selected text, and some strings that represent the author and the description of the text.
To define a comment adornment
In the CommentAdornmentTest project, add a new class file and name it
CommentAdornment
.Add the following references:
Microsoft.VisualStudio.CoreUtility
Microsoft.VisualStudio.Text.Data
Microsoft.VisualStudio.Text.Logic
Microsoft.VisualStudio.Text.UI
Microsoft.VisualStudio.Text.UI.Wpf
System.ComponentModel.Composition
PresentationCore
PresentationFramework
WindowsBase
Add the following
using
directive.using Microsoft.VisualStudio.Text;
The file should contain a class named
CommentAdornment
.internal class CommentAdornment
Add three fields to the
CommentAdornment
class for the ITrackingSpan, the author, and the description.public readonly ITrackingSpan Span; public readonly string Author; public readonly string Text;
Add a constructor that initializes the fields.
public CommentAdornment(SnapshotSpan span, string author, string text) { this.Span = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeExclusive); this.Author = author; this.Text = text; }
Create a visual element for the adornment
Define a visual element for your adornment. For this walkthrough, define a control that inherits from the Windows Presentation Foundation (WPF) class Canvas.
Create a class in the CommentAdornmentTest project, and name it
CommentBlock
.Add the following
using
directives.using Microsoft.VisualStudio.Text; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
Make the
CommentBlock
class inherit from Canvas.internal class CommentBlock : Canvas { }
Add some private fields to define the visual aspects of the adornment.
private Geometry textGeometry; private Grid commentGrid; private static Brush brush; private static Pen solidPen; private static Pen dashPen;
Add a constructor that defines the comment adornment and adds the relevant text.
public CommentBlock(double textRightEdge, double viewRightEdge, Geometry newTextGeometry, string author, string body) { if (brush == null) { brush = new SolidColorBrush(Color.FromArgb(0x20, 0x00, 0xff, 0x00)); brush.Freeze(); Brush penBrush = new SolidColorBrush(Colors.Green); penBrush.Freeze(); solidPen = new Pen(penBrush, 0.5); solidPen.Freeze(); dashPen = new Pen(penBrush, 0.5); dashPen.DashStyle = DashStyles.Dash; dashPen.Freeze(); } this.textGeometry = newTextGeometry; TextBlock tb1 = new TextBlock(); tb1.Text = author; TextBlock tb2 = new TextBlock(); tb2.Text = body; const int MarginWidth = 8; this.commentGrid = new Grid(); this.commentGrid.RowDefinitions.Add(new RowDefinition()); this.commentGrid.RowDefinitions.Add(new RowDefinition()); ColumnDefinition cEdge = new ColumnDefinition(); cEdge.Width = new GridLength(MarginWidth); ColumnDefinition cEdge2 = new ColumnDefinition(); cEdge2.Width = new GridLength(MarginWidth); this.commentGrid.ColumnDefinitions.Add(cEdge); this.commentGrid.ColumnDefinitions.Add(new ColumnDefinition()); this.commentGrid.ColumnDefinitions.Add(cEdge2); System.Windows.Shapes.Rectangle rect = new System.Windows.Shapes.Rectangle(); rect.RadiusX = 6; rect.RadiusY = 3; rect.Fill = brush; rect.Stroke = Brushes.Green; Size inf = new Size(double.PositiveInfinity, double.PositiveInfinity); tb1.Measure(inf); tb2.Measure(inf); double middleWidth = Math.Max(tb1.DesiredSize.Width, tb2.DesiredSize.Width); this.commentGrid.Width = middleWidth + 2 * MarginWidth; Grid.SetColumn(rect, 0); Grid.SetRow(rect, 0); Grid.SetRowSpan(rect, 2); Grid.SetColumnSpan(rect, 3); Grid.SetRow(tb1, 0); Grid.SetColumn(tb1, 1); Grid.SetRow(tb2, 1); Grid.SetColumn(tb2, 1); this.commentGrid.Children.Add(rect); this.commentGrid.Children.Add(tb1); this.commentGrid.Children.Add(tb2); Canvas.SetLeft(this.commentGrid, Math.Max(viewRightEdge - this.commentGrid.Width - 20.0, textRightEdge + 20.0)); Canvas.SetTop(this.commentGrid, textGeometry.GetRenderBounds(solidPen).Top); this.Children.Add(this.commentGrid); }
Also implement an OnRender event handler that draws the adornment.
protected override void OnRender(DrawingContext dc) { base.OnRender(dc); if (this.textGeometry != null) { dc.DrawGeometry(brush, solidPen, this.textGeometry); Rect textBounds = this.textGeometry.GetRenderBounds(solidPen); Point p1 = new Point(textBounds.Right, textBounds.Bottom); Point p2 = new Point(Math.Max(Canvas.GetLeft(this.commentGrid) - 20.0, p1.X), p1.Y); Point p3 = new Point(Math.Max(Canvas.GetLeft(this.commentGrid), p1.X), (Canvas.GetTop(this.commentGrid) + p1.Y) * 0.5); dc.DrawLine(dashPen, p1, p2); dc.DrawLine(dashPen, p2, p3); } }
Add an IWpfTextViewCreationListener
The IWpfTextViewCreationListener is a MEF component part that you can use to listen to view creation events.
Add a class file to the CommentAdornmentTest project and name it
Connector
.Add the following
using
directives.using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
Declare a class that implements IWpfTextViewCreationListener, and export it with a ContentTypeAttribute of "text" and a TextViewRoleAttribute of Document. The content type attribute specifies the kind of content to which the component applies. The text type is the base type for all non-binary file types. Therefore, almost every text view that is created will be of this type. The text view role attribute specifies the kind of text view to which the component applies. Document text view roles generally show text that is composed of lines and is stored in a file.
Implement the TextViewCreated method so that it calls the static
Create()
event of theCommentAdornmentManager
.public void TextViewCreated(IWpfTextView textView) { CommentAdornmentManager.Create(textView); }
Add a method that you can use to execute the command.
static public void Execute(IWpfTextViewHost host) { IWpfTextView view = host.TextView; //Add a comment on the selected text. if (!view.Selection.IsEmpty) { //Get the provider for the comment adornments in the property bag of the view. CommentAdornmentProvider provider = view.Properties.GetProperty<CommentAdornmentProvider>(typeof(CommentAdornmentProvider)); //Add some arbitrary author and comment text. string author = System.Security.Principal.WindowsIdentity.GetCurrent().Name; string comment = "Four score...."; //Add the comment adornment using the provider. provider.Add(view.Selection.SelectedSpans[0], author, comment); } }
Define an adornment layer
To add a new adornment, you must define an adornment layer.
To define an adornment layer
In the
Connector
class, declare a public field of type AdornmentLayerDefinition, and export it with a NameAttribute that specifies a unique name for the adornment layer and an OrderAttribute that defines the Z-order relationship of this adornment layer to the other text view layers (text, caret, and selection).[Export(typeof(AdornmentLayerDefinition))] [Name("CommentAdornmentLayer")] [Order(After = PredefinedAdornmentLayers.Selection, Before = PredefinedAdornmentLayers.Text)] public AdornmentLayerDefinition commentLayerDefinition;
Provide comment adornments
When you define an adornment, also implement a comment adornment provider and a comment adornment manager. The comment adornment provider keeps a list of comment adornments, listens to Changed events on the underlying text buffer, and deletes comment adornments when the underlying text is deleted.
Add a new class file to the CommentAdornmentTest project and name it
CommentAdornmentProvider
.Add the following
using
directives.using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor;
Add a class named
CommentAdornmentProvider
.internal class CommentAdornmentProvider { }
Add private fields for the text buffer and the list of comment adornments related to the buffer.
private ITextBuffer buffer; private IList<CommentAdornment> comments = new List<CommentAdornment>();
Add a constructor for
CommentAdornmentProvider
. This constructor should have private access because the provider is instantiated by theCreate()
method. The constructor adds theOnBufferChanged
event handler to the Changed event.private CommentAdornmentProvider(ITextBuffer buffer) { this.buffer = buffer; //listen to the Changed event so we can react to deletions. this.buffer.Changed += OnBufferChanged; }
Add the
Create()
method.public static CommentAdornmentProvider Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentProvider>(delegate { return new CommentAdornmentProvider(view.TextBuffer); }); }
Add the
Detach()
method.public void Detach() { if (this.buffer != null) { //remove the Changed listener this.buffer.Changed -= OnBufferChanged; this.buffer = null; } }
Add the
OnBufferChanged
event handler.private void OnBufferChanged(object sender, TextContentChangedEventArgs e) { //Make a list of all comments that have a span of at least one character after applying the change. There is no need to raise a changed event for the deleted adornments. The adornments are deleted only if a text change would cause the view to reformat the line and discard the adornments. IList<CommentAdornment> keptComments = new List<CommentAdornment>(this.comments.Count); foreach (CommentAdornment comment in this.comments) { Span span = comment.Span.GetSpan(e.After); //if a comment does not span at least one character, its text was deleted. if (span.Length != 0) { keptComments.Add(comment); } } this.comments = keptComments; }
Add a declaration for a
CommentsChanged
event.public event EventHandler<CommentsChangedEventArgs> CommentsChanged;
Create an
Add()
method to add the adornment.public void Add(SnapshotSpan span, string author, string text) { if (span.Length == 0) throw new ArgumentOutOfRangeException("span"); if (author == null) throw new ArgumentNullException("author"); if (text == null) throw new ArgumentNullException("text"); //Create a comment adornment given the span, author and text. CommentAdornment comment = new CommentAdornment(span, author, text); //Add it to the list of comments. this.comments.Add(comment); //Raise the changed event. EventHandler<CommentsChangedEventArgs> commentsChanged = this.CommentsChanged; if (commentsChanged != null) commentsChanged(this, new CommentsChangedEventArgs(comment, null)); }
Add a
RemoveComments()
method.public void RemoveComments(SnapshotSpan span) { EventHandler<CommentsChangedEventArgs> commentsChanged = this.CommentsChanged; //Get a list of all the comments that are being kept IList<CommentAdornment> keptComments = new List<CommentAdornment>(this.comments.Count); foreach (CommentAdornment comment in this.comments) { //find out if the given span overlaps with the comment text span. If two spans are adjacent, they do not overlap. To consider adjacent spans, use IntersectsWith. if (comment.Span.GetSpan(span.Snapshot).OverlapsWith(span)) { //Raise the change event to delete this comment. if (commentsChanged != null) commentsChanged(this, new CommentsChangedEventArgs(null, comment)); } else keptComments.Add(comment); } this.comments = keptComments; }
Add a
GetComments()
method that returns all the comments in a given snapshot span.public Collection<CommentAdornment> GetComments(SnapshotSpan span) { IList<CommentAdornment> overlappingComments = new List<CommentAdornment>(); foreach (CommentAdornment comment in this.comments) { if (comment.Span.GetSpan(span.Snapshot).OverlapsWith(span)) overlappingComments.Add(comment); } return new Collection<CommentAdornment>(overlappingComments); }
Add a class named
CommentsChangedEventArgs
, as follows.internal class CommentsChangedEventArgs : EventArgs { public readonly CommentAdornment CommentAdded; public readonly CommentAdornment CommentRemoved; public CommentsChangedEventArgs(CommentAdornment added, CommentAdornment removed) { this.CommentAdded = added; this.CommentRemoved = removed; } }
Manage comment adornments
The comment adornment manager creates the adornment and adds it to the adornment layer. It listens to the LayoutChanged and Closed events so that it can move or delete the adornment. It also listens to the CommentsChanged
event that is fired by the comment adornment provider when comments are added or removed.
Add a class file to the CommentAdornmentTest project and name it
CommentAdornmentManager
.Add the following
using
directives.using System; using System.Collections.Generic; using System.Windows.Media; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Formatting;
Add a class named
CommentAdornmentManager
.internal class CommentAdornmentManager { }
Add some private fields.
private readonly IWpfTextView view; private readonly IAdornmentLayer layer; private readonly CommentAdornmentProvider provider;
Add a constructor that subscribes the manager to the LayoutChanged and Closed events, and also to the
CommentsChanged
event. The constructor is private because the manager is instantiated by the staticCreate()
method.private CommentAdornmentManager(IWpfTextView view) { this.view = view; this.view.LayoutChanged += OnLayoutChanged; this.view.Closed += OnClosed; this.layer = view.GetAdornmentLayer("CommentAdornmentLayer"); this.provider = CommentAdornmentProvider.Create(view); this.provider.CommentsChanged += OnCommentsChanged; }
Add the
Create()
method that gets a provider or creates one if necessary.public static CommentAdornmentManager Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentManager>(delegate { return new CommentAdornmentManager(view); }); }
Add the
CommentsChanged
handler.private void OnCommentsChanged(object sender, CommentsChangedEventArgs e) { //Remove the comment (when the adornment was added, the comment adornment was used as the tag). if (e.CommentRemoved != null) this.layer.RemoveAdornmentsByTag(e.CommentRemoved); //Draw the newly added comment (this will appear immediately: the view does not need to do a layout). if (e.CommentAdded != null) this.DrawComment(e.CommentAdded); }
Add the Closed handler.
private void OnClosed(object sender, EventArgs e) { this.provider.Detach(); this.view.LayoutChanged -= OnLayoutChanged; this.view.Closed -= OnClosed; }
Add the LayoutChanged handler.
private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { //Get all of the comments that intersect any of the new or reformatted lines of text. List<CommentAdornment> newComments = new List<CommentAdornment>(); //The event args contain a list of modified lines and a NormalizedSpanCollection of the spans of the modified lines. //Use the latter to find the comments that intersect the new or reformatted lines of text. foreach (Span span in e.NewOrReformattedSpans) { newComments.AddRange(this.provider.GetComments(new SnapshotSpan(this.view.TextSnapshot, span))); } //It is possible to get duplicates in this list if a comment spanned 3 lines, and the first and last lines were modified but the middle line was not. //Sort the list and skip duplicates. newComments.Sort(delegate(CommentAdornment a, CommentAdornment b) { return a.GetHashCode().CompareTo(b.GetHashCode()); }); CommentAdornment lastComment = null; foreach (CommentAdornment comment in newComments) { if (comment != lastComment) { lastComment = comment; this.DrawComment(comment); } } }
Add the private method that draws the comment.
private void DrawComment(CommentAdornment comment) { SnapshotSpan span = comment.Span.GetSpan(this.view.TextSnapshot); Geometry g = this.view.TextViewLines.GetMarkerGeometry(span); if (g != null) { //Find the rightmost coordinate of all the lines that intersect the adornment. double maxRight = 0.0; foreach (ITextViewLine line in this.view.TextViewLines.GetTextViewLinesIntersectingSpan(span)) maxRight = Math.Max(maxRight, line.Right); //Create the visualization. CommentBlock block = new CommentBlock(maxRight, this.view.ViewportRight, g, comment.Author, comment.Text); //Add it to the layer. this.layer.AddAdornment(span, comment, block); } }
Use the menu command to add the comment adornment
You can use the menu command to create a comment adornment by implementing the MenuItemCallback
method of the VSPackage.
Add the following references to the MenuCommandTest project:
Microsoft.VisualStudio.TextManager.Interop
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Text.UI.Wpf
Open the AddAdornment.cs file and add the following
using
directives.using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Editor; using CommentAdornmentTest;
Delete the
Execute()
method and add the following command handler.private async void AddAdornmentHandler(object sender, EventArgs e) { }
Add code to get the active view. You must get the
SVsTextManager
of the Visual Studio shell to get the activeIVsTextView
.private async void AddAdornmentHandler(object sender, EventArgs e) { IVsTextManager txtMgr = (IVsTextManager) await ServiceProvider.GetServiceAsync(typeof(SVsTextManager)); IVsTextView vTextView = null; int mustHaveFocus = 1; txtMgr.GetActiveView(mustHaveFocus, null, out vTextView); }
If this text view is an instance of an editor text view, you can cast it to the IVsUserData interface and then get the IWpfTextViewHost and its associated IWpfTextView. Use the IWpfTextViewHost to call the
Connector.Execute()
method, which gets the comment adornment provider and adds the adornment. The command handler should now look like this code:private async void AddAdornmentHandler(object sender, EventArgs e) { IVsTextManager txtMgr = (IVsTextManager) await ServiceProvider.GetServiceAsync(typeof(SVsTextManager)); IVsTextView vTextView = null; int mustHaveFocus = 1; txtMgr.GetActiveView(mustHaveFocus, null, out vTextView); IVsUserData userData = vTextView as IVsUserData; if (userData == null) { Console.WriteLine("No text view is currently open"); return; } IWpfTextViewHost viewHost; object holder; Guid guidViewHost = DefGuidList.guidIWpfTextViewHost; userData.GetData(ref guidViewHost, out holder); viewHost = (IWpfTextViewHost)holder; Connector.Execute(viewHost); }
Set the AddAdornmentHandler method as the handler for the AddAdornment command in the AddAdornment constructor.
private AddAdornment(AsyncPackage package, OleMenuCommandService commandService) { this.package = package ?? throw new ArgumentNullException(nameof(package)); commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); var menuCommandID = new CommandID(CommandSet, CommandId); var menuItem = new MenuCommand(this.AddAdornmentHandler, menuCommandID); commandService.AddCommand(menuItem); }
Build and test the code
Build the solution and start debugging. The experimental instance should appear.
Create a text file. Type some text and then select it.
On the Tools menu, click Invoke Add Adornment. A balloon should display on the right side of the text window, and should contain text that resembles the following text.
YourUserName
Fourscore...