Walkthrough: Using 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.
Prerequisites
To follow this walkthrough, you must install the Visual Studio 2013 SDK. For more information, see Visual Studio Software Development Kit (SDK).
Locations for the Visual Studio Package Project Template
The Visual Studio Package project template can be found in three different locations in the New Project dialog:
Under Visual Basic, Extensibility. The default language of the project is Visual Basic.
Under C#, Extensibility. The default language of the project is C#.
Under Other Project Types, Extensibility. The default language of the project is C++
Creating a Menu Command VSPackage
Create a VSPackage that puts a menu command named Add Adornment on the Tools menu.
To create a menu command VSPackage
Create a Visual Studio Package and name it MenuCommandTest. Click OK.
On the welcome page, click Next.
On the Select a Programming Language page, select Visual Basic or Visual C#, make sure that Generate a new key file to sign the assembly is selected, and then click Next.
On the Basic VSPackage Information page, keep the current values, and then click Next.
On the Select VSPackage Options page, select Menu Command and then click Next.
On the Command Options page, in the Command name text box, type Add Adornment. In the Command ID text box, type cmdidAddAdornment. Click Next.
On the Select Test Options page, clear both options and then click Finish.
A solution named MenuCommandTest is opened. 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 be displayed. Later steps 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.
Adding a MEF Extension to the VSPackage Solution
To add the MEF extension to the VSPackage solution
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 Basic or Visual C#, then EditorClassifier. Name the project CommentAdornmentTest.
Because this project will interact with the strongly-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 page.
Select Sign the assembly.
Under Choose a strong name key file, select the Key.snk file that was generated for the MenuCommandTest assembly.
Save the project.
Referring 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.
Add a reference to the CommentAdornmentTest project.
Defining 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, delete the existing class files.
Add a new class file and name it CommentAdornment.
Add the following using statement.
Imports Microsoft.VisualStudio.Text
using Microsoft.VisualStudio.Text;
Add a class named CommentAdornment.
Friend Class CommentAdornment
internal class CommentAdornment
Add three fields to the CommentAdornment class for the ITrackingSpan, the author, and the description.
Public ReadOnly Span As ITrackingSpan Public ReadOnly Author As String Public ReadOnly Text As String
public readonly ITrackingSpan Span; public readonly string Author; public readonly string Text;
Add a constructor that initializes the fields.
Public Sub New(ByVal span As SnapshotSpan, ByVal author As String, ByVal text As String) Me.Span = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeExclusive) Me.Author = author Me.Text = text End Sub
public CommentAdornment(SnapshotSpan span, string author, string text) { this.Span = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeExclusive); this.Author = author; this.Text = text; }
Creating a Visual Element for the Adornment
You must also define a visual element for your adornment. For this walkthrough, define a control that inherits from the Windows Presentation Foundation (WPF) class Canvas.
To create a visual element for the adornment
Create a class in the CommentAdornmentTest project, and name it CommentBlock.
Add the following using statements.
Imports System Imports System.Windows Imports System.Windows.Controls Imports System.Windows.Media Imports System.Windows.Shapes
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes;
Make the CommentBlock class inherit from Canvas.
Friend Class CommentBlock Inherits Canvas
internal class CommentBlock : Canvas
Add some private fields to define the visual aspects of the adornment.
Private textGeometry As Geometry Private commentGrid As Grid Private Shared brush As Brush Private Shared solidPen As Pen Private Shared dashPen As Pen
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 Sub New(ByVal textRightEdge As Double, ByVal viewRightEdge As Double, ByVal newTextGeometry As Geometry, ByVal author As String, ByVal body As String) If brush Is Nothing Then brush = New SolidColorBrush(Color.FromArgb(&H20, &H0, &HFF, &H0)) brush.Freeze() Dim penBrush As Brush = 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() End If Me.textGeometry = newTextGeometry Dim tb1 As New TextBlock() tb1.Text = author Dim tb2 As New TextBlock() tb2.Text = body Const MarginWidth As Integer = 8 Me.commentGrid = New Grid() Me.commentGrid.RowDefinitions.Add(New RowDefinition()) Me.commentGrid.RowDefinitions.Add(New RowDefinition()) Dim cEdge As New ColumnDefinition() cEdge.Width = New GridLength(MarginWidth) Dim cEdge2 As New ColumnDefinition() cEdge2.Width = New GridLength(MarginWidth) Me.commentGrid.ColumnDefinitions.Add(cEdge) Me.commentGrid.ColumnDefinitions.Add(New ColumnDefinition()) Me.commentGrid.ColumnDefinitions.Add(cEdge2) Dim rect As New System.Windows.Shapes.Rectangle() rect.RadiusX = 6 rect.RadiusY = 3 rect.Fill = brush rect.Stroke = Brushes.Green Dim inf As New Size(Double.PositiveInfinity, Double.PositiveInfinity) tb1.Measure(inf) tb2.Measure(inf) Dim middleWidth As Double = Math.Max(tb1.DesiredSize.Width, tb2.DesiredSize.Width) Me.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) Me.commentGrid.Children.Add(rect) Me.commentGrid.Children.Add(tb1) Me.commentGrid.Children.Add(tb2) Canvas.SetLeft(Me.commentGrid, Math.Max(viewRightEdge - Me.commentGrid.Width - 20.0, textRightEdge + 20.0)) Canvas.SetTop(Me.commentGrid, textGeometry.GetRenderBounds(solidPen).Top) Me.Children.Add(Me.commentGrid) End Sub
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 Overrides Sub OnRender(ByVal dc As DrawingContext) MyBase.OnRender(dc) If Me.textGeometry IsNot Nothing Then dc.DrawGeometry(brush, solidPen, Me.textGeometry) Dim textBounds As Rect = Me.textGeometry.GetRenderBounds(solidPen) Dim p1 As New Point(textBounds.Right, textBounds.Bottom) Dim p2 As New Point(Math.Max(Canvas.GetLeft(Me.commentGrid) - 20.0, p1.X), p1.Y) Dim p3 As New Point(Math.Max(Canvas.GetLeft(Me.commentGrid), p1.X), (Canvas.GetTop(Me.commentGrid) + p1.Y) * 0.5) dc.DrawLine(dashPen, p1, p2) dc.DrawLine(dashPen, p2, p3) End If End Sub
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); } }
Adding an IWpfTextViewCreationListener
The IWpfTextViewCreationListener is a MEF component part that you can use to listen to view creation events.
To add an IWpfTextViewCreationListener
Add a class file to the CommentAdornmentTest project and name it Connector.
Add the following using statements.
Imports System.ComponentModel.Composition Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Utilities
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.
<Export(GetType(IWpfTextViewCreationListener)), ContentType("text"), TextViewRole(PredefinedTextViewRoles.Document)> Public NotInheritable Class Connector Implements IWpfTextViewCreationListener
[Export(typeof(IWpfTextViewCreationListener))] [ContentType("text")] [TextViewRole(PredefinedTextViewRoles.Document)] public sealed class Connector : IWpfTextViewCreationListener
Implement the TextViewCreated method so that it calls the static Create() event of the CommentAdornmentManager.
Public Sub TextViewCreated(ByVal textView As IWpfTextView) Implements IWpfTextViewCreationListener.TextViewCreated CommentAdornmentManager.Create(textView) End Sub
public void TextViewCreated(IWpfTextView textView) { CommentAdornmentManager.Create(textView); }
Add a method that you can use to execute the command.
Public Shared Sub Execute(ByVal host As IWpfTextViewHost) Dim view As IWpfTextView = host.TextView 'Add a comment on the selected text. If Not view.Selection.IsEmpty Then 'Get the provider for the comment adornments in the property bag of the view. Dim provider As CommentAdornmentProvider = view.Properties.GetProperty(Of CommentAdornmentProvider)(GetType(CommentAdornmentProvider)) 'Add some arbitrary author and comment text. Dim author As String = System.Security.Principal.WindowsIdentity.GetCurrent().Name Dim comment As String = "Four score...." 'Add the comment adornment using the provider. provider.Add(view.Selection.SelectedSpans(0), author, comment) End If End Sub
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); } }
Defining 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(GetType(AdornmentLayerDefinition)), Name("CommentAdornmentLayer"), Order(After:=PredefinedAdornmentLayers.Selection, Before:=PredefinedAdornmentLayers.Text)> Public commentLayerDefinition As AdornmentLayerDefinition
[Export(typeof(AdornmentLayerDefinition))] [Name("CommentAdornmentLayer")] [Order(After = PredefinedAdornmentLayers.Selection, Before = PredefinedAdornmentLayers.Text)] public AdornmentLayerDefinition commentLayerDefinition;
Providing 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.
To add the comment adornment provider
Add a new class file to the CommentAdornmentTest project and name it CommentAdornmentProvider.
Add the following using statements.
Imports System Imports System.Collections.Generic Imports System.Collections.ObjectModel Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Editor
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor;
Add a class named CommentAdornmentProvider.
Friend Class CommentAdornmentProvider
internal class CommentAdornmentProvider
Add private fields for the text buffer and the list of comment adornments related to the buffer.
Private buffer As ITextBuffer Private comments As IList(Of CommentAdornment) = New List(Of CommentAdornment)()
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 the Create() method. The constructor adds the OnBufferChanged event handler to the Changed event.
Private Sub New(ByVal buffer As ITextBuffer) Me.buffer = buffer 'listen to the Changed event so we can react to deletions. AddHandler Me.buffer.Changed, AddressOf OnBufferChanged End Sub
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 Shared Function Create(ByVal view As IWpfTextView) As CommentAdornmentProvider Return view.Properties.GetOrCreateSingletonProperty(Of CommentAdornmentProvider)(Function() New CommentAdornmentProvider(view.TextBuffer)) End Function
public static CommentAdornmentProvider Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentProvider>(delegate { return new CommentAdornmentProvider(view.TextBuffer); }); }
Add the Detach() method.
Public Sub Detach() If Me.buffer IsNot Nothing Then 'remove the Changed listener RemoveHandler Me.buffer.Changed, AddressOf OnBufferChanged Me.buffer = Nothing End If End Sub
public void Detach() { if (this.buffer != null) { //remove the Changed listener this.buffer.Changed -= OnBufferChanged; this.buffer = null; } }
Add the OnBufferChanged event handler.
Private Sub OnBufferChanged(ByVal sender As Object, ByVal e As TextContentChangedEventArgs) '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. Dim keptComments As IList(Of CommentAdornment) = New List(Of CommentAdornment)(Me.comments.Count) For Each comment As CommentAdornment In Me.comments Dim span As Span = comment.Span.GetSpan(e.After) 'if a comment does not span at least one character, its text was deleted. If span.Length <> 0 Then keptComments.Add(comment) End If Next comment Me.comments = keptComments End Sub
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 CommentsChanged As EventHandler(Of CommentsChangedEventArgs)
public event EventHandler<CommentsChangedEventArgs> CommentsChanged;
Create an Add() method to add the adornment.
Public Sub Add(ByVal span As SnapshotSpan, ByVal author As String, ByVal text As String) If span.Length = 0 Then Throw New ArgumentOutOfRangeException("span") End If If author Is Nothing Then Throw New ArgumentNullException("author") End If If text Is Nothing Then Throw New ArgumentNullException("text") End If 'Create a comment adornment given the span, author and text. Dim comment As New CommentAdornment(span, author, text) 'Add it to the list of comments. Me.comments.Add(comment) 'Raise the changed event. Dim commentsChanged As EventHandler(Of CommentsChangedEventArgs) = Me.CommentsChangedEvent If CommentsChangedEvent IsNot Nothing Then CommentsChangedEvent(Me, New CommentsChangedEventArgs(comment, Nothing)) End If End Sub
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 Sub RemoveComments(ByVal span As SnapshotSpan) Dim commentsChanged As EventHandler(Of CommentsChangedEventArgs) = Me.CommentsChangedEvent 'Get a list of all the comments that are being kept Dim keptComments As IList(Of CommentAdornment) = New List(Of CommentAdornment)(Me.comments.Count) For Each comment As CommentAdornment In Me.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) Then 'Raise the change event to delete this comment. If CommentsChangedEvent IsNot Nothing Then CommentsChangedEvent(Me, New CommentsChangedEventArgs(Nothing, comment)) End If Else keptComments.Add(comment) End If Next comment Me.comments = keptComments End Sub
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 Function GetComments(ByVal span As SnapshotSpan) As Collection(Of CommentAdornment) Dim overlappingComments As IList(Of CommentAdornment) = New List(Of CommentAdornment)() For Each comment As CommentAdornment In Me.comments If comment.Span.GetSpan(span.Snapshot).OverlapsWith(span) Then overlappingComments.Add(comment) End If Next comment Return New Collection(Of CommentAdornment)(overlappingComments) End Function
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.
Friend Class CommentsChangedEventArgs Inherits EventArgs Public ReadOnly CommentAdded As CommentAdornment Public ReadOnly CommentRemoved As CommentAdornment Public Sub New(ByVal added As CommentAdornment, ByVal removed As CommentAdornment) Me.CommentAdded = added Me.CommentRemoved = removed End Sub End Class
internal class CommentsChangedEventArgs : EventArgs { public readonly CommentAdornment CommentAdded; public readonly CommentAdornment CommentRemoved; public CommentsChangedEventArgs(CommentAdornment added, CommentAdornment removed) { this.CommentAdded = added; this.CommentRemoved = removed; } }
Managing 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.
To manage comment adornments
Add a class file to the CommentAdornmentTest project and name it CommentAdornmentManager.
Add the following using statements.
Imports System Imports System.Collections.Generic Imports System.Windows.Media Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Text.Formatting
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.
Friend Class CommentAdornmentManager
internal class CommentAdornmentManager
Add some private fields.
Private ReadOnly view As IWpfTextView Private ReadOnly layer As IAdornmentLayer Private ReadOnly provider As CommentAdornmentProvider
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 static Create() method.
Private Sub New(ByVal view As IWpfTextView) Me.view = view AddHandler Me.view.LayoutChanged, AddressOf OnLayoutChanged AddHandler Me.view.Closed, AddressOf OnClosed Me.layer = view.GetAdornmentLayer("CommentAdornmentLayer") Me.provider = CommentAdornmentProvider.Create(view) AddHandler Me.provider.CommentsChanged, AddressOf OnCommentsChanged End Sub
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 required.
Public Shared Function Create(ByVal view As IWpfTextView) As CommentAdornmentManager Return view.Properties.GetOrCreateSingletonProperty(Of CommentAdornmentManager)(Function() New CommentAdornmentManager(view)) End Function
public static CommentAdornmentManager Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentManager>(delegate { return new CommentAdornmentManager(view); }); }
Add the CommentsChanged handler.
Private Sub OnCommentsChanged(ByVal sender As Object, ByVal e As CommentsChangedEventArgs) 'Remove the comment (when the adornment was added, the comment adornment was used as the tag). If e.CommentRemoved IsNot Nothing Then Me.layer.RemoveAdornmentsByTag(e.CommentRemoved) End If 'Draw the newly added comment (this will appear immediately: the view does not need to do a layout). If e.CommentAdded IsNot Nothing Then Me.DrawComment(e.CommentAdded) End If End Sub
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 Sub OnClosed(ByVal sender As Object, ByVal e As EventArgs) Me.provider.Detach() RemoveHandler Me.view.LayoutChanged, AddressOf OnLayoutChanged RemoveHandler Me.view.Closed, AddressOf OnClosed End Sub
private void OnClosed(object sender, EventArgs e) { this.provider.Detach(); this.view.LayoutChanged -= OnLayoutChanged; this.view.Closed -= OnClosed; }
Add the LayoutChanged handler.
Private Sub OnLayoutChanged(ByVal sender As Object, ByVal e As TextViewLayoutChangedEventArgs) 'Get all of the comments that intersect any of the new or reformatted lines of text. Dim newComments As New List(Of 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. For Each span As Span In e.NewOrReformattedSpans newComments.AddRange(Me.provider.GetComments(New SnapshotSpan(Me.view.TextSnapshot, span))) Next 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(Function(a As CommentAdornment, b As CommentAdornment) a.GetHashCode().CompareTo(b.GetHashCode())) Dim lastComment As CommentAdornment = Nothing For Each comment As CommentAdornment In newComments If comment IsNot lastComment Then lastComment = comment Me.DrawComment(comment) End If Next comment End Sub
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 Sub DrawComment(ByVal comment As CommentAdornment) Dim span As SnapshotSpan = comment.Span.GetSpan(Me.view.TextSnapshot) Dim g As Geometry = Me.view.TextViewLines.GetMarkerGeometry(span) If g IsNot Nothing Then 'Find the rightmost coordinate of all the lines that intersect the adornment. Dim maxRight As Double = 0.0 For Each line As ITextViewLine In Me.view.TextViewLines.GetTextViewLinesIntersectingSpan(span) maxRight = Math.Max(maxRight, line.Right) Next line 'Create the visualization. Dim block As New CommentBlock(maxRight, Me.view.ViewportRight, g, comment.Author, comment.Text) 'Add it to the layer. Me.layer.AddAdornment(span, comment, block) End If End Sub
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); } }
Using 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.
To use the menu command to add the comment adornment
Add the following references to the MenuCommandTest project:
Microsoft.VisualStudio.TextManager.Interop
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Text.UI.Wpf
Add a reference to the CommentAdornmentTest project.
Open the MenuCommandTestPackage file.
Add the following using statements.
Imports Microsoft.VisualStudio.TextManager.Interop Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Editor Imports CommentAdornmentTest
using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Editor; using CommentAdornmentTest;
In the MenuItemCallback method, remove the existing code.
Add code to get the active view. You must get the SVsTextManager of the Visual Studio shell to get the active IVsTextView.
Dim txtMgr As IVsTextManager = CType(GetService(GetType(SVsTextManager)), IVsTextManager) Dim vTextView As IVsTextView = Nothing Dim mustHaveFocus As Integer = 1 txtMgr.GetActiveView(mustHaveFocus, Nothing, vTextView)
IVsTextManager txtMgr = (IVsTextManager)GetService(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.
Dim userData As IVsUserData = TryCast(vTextView, IVsUserData) If userData Is Nothing Then Console.WriteLine("No text view is currently open") Return End If Dim viewHost As IWpfTextViewHost Dim holder As Object Dim guidViewHost As Guid = DefGuidList.guidIWpfTextViewHost userData.GetData(guidViewHost, holder) viewHost = CType(holder, IWpfTextViewHost)
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;
Use the IWpfTextViewHost to call the Connector.Execute() method, which gets the comment adornment provider and adds the adornment.
Connector.Execute(viewHost)
Connector.Execute(viewHost);
Building and Testing the Code
To test this code, build the MenuCommand solution and run it in the experimental instance.
To build and test the MenuCommand solution
Build the solution. When you run this project in the debugger, a second instance of Visual Studio is instantiated.
Create a text file. Type some text and then select it.
On the Tools menu, click Add Adornment. A balloon should be displayed on the right side of the text window, and should contain text that resembles the following text.
YourUserName
Fourscore…
See Also
Tasks
Walkthrough: Linking a Content Type to a File Name Extension