Walkthrough: Displaying SmartTags
Smart tags are tags on text that expand to display a set of actions. For example, in a Visual Basic or Visual C# project, a red line appears under a word when you rename an identifier such as a variable name. When you move the pointer over the underline, a button is displayed near the pointer. If you click the button, a suggested action is displayed, for example, Rename IsRead to IsReady. If you click the action, all references to IsRead in the project are renamed IsReady.
Although smart tags are part of the IntelliSense implementation in the editor, you can implement smart tags by subclassing SmartTag, and then implementing the ITagger<T> interface and the IViewTaggerProvider interface.
Note
Other kinds of tags can be implemented in a similar manner.
The following walkthrough shows how to create a smart tag that appears on the current word and has two suggested actions: Convert to upper case and Convert to lower case.
Prerequisites
To follow this walkthrough, you must install the Visual Studio 2013 SDK. For more information, see Visual Studio Software Development Kit (SDK).
Creating a Managed Extensibility Framework (MEF) Project
To create a MEF project
Create an Editor Classifier project. Name the solution SmartTagTest.
Open the source.extension.vsixmanifest file in the VSIX Manifest Editor.
Make sure that the Assets section contains a Microsoft.VisualStudio.MefComponent type, the Source is set to A project in current solution, and Project is set to SmartTagTest.dll.
Save and close source.extension.vsixmanifest.
Add the following reference to the project, and set CopyLocal to false:
Microsoft.VisualStudio.Language.Intellisense
Delete the existing class files.
Implementing a Tagger for Smart Tags
To implement a tagger for smart tags
Add a class file and name it TestSmartTag.
Add the following imports:
Imports System Imports System.Collections.Generic Imports System.ComponentModel.Composition Imports System.Collections.ObjectModel Imports System.Windows.Media Imports Microsoft.VisualStudio.Language.Intellisense Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Text.Operations Imports Microsoft.VisualStudio.Text.Tagging Imports Microsoft.VisualStudio.Utilities
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Collections.ObjectModel; using System.Windows.Media; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities;
Add a class named TestSmartTag that inherits from SmartTag.
Friend Class TestSmartTag Inherits SmartTag
internal class TestSmartTag : SmartTag
Add a constructor for this class that calls the base constructor with a SmartTagType of Factoid, which will cause a blue line to appear under the first character of a word. (If you use Ephemeral, a red line will appear under the last character of the word.)
Public Sub New(ByVal actionSets As ReadOnlyCollection(Of SmartTagActionSet)) MyBase.New(SmartTagType.Factoid, actionSets) End Sub
public TestSmartTag(ReadOnlyCollection<SmartTagActionSet> actionSets) : base(SmartTagType.Factoid, actionSets) { }
Add a class named TestSmartTagger that inherits from ITagger<T> of type TestSmartTag, and implements IDisposable.
Friend Class TestSmartTagger Implements ITagger(Of TestSmartTag), IDisposable
internal class TestSmartTagger : ITagger<TestSmartTag>, IDisposable
Add the following private fields to the tagger class.
Private m_buffer As ITextBuffer Private m_view As ITextView Private m_provider As TestSmartTaggerProvider Private m_disposed As Boolean
private ITextBuffer m_buffer; private ITextView m_view; private TestSmartTaggerProvider m_provider; private bool m_disposed;
Add a constructor that sets the private fields, and subscribes to the LayoutChanged event.
Public Sub New(ByVal buffer As ITextBuffer, ByVal view As ITextView, ByVal provider As TestSmartTaggerProvider) m_buffer = buffer m_view = view m_provider = provider AddHandler m_view.LayoutChanged, AddressOf OnLayoutChanged End Sub
public TestSmartTagger(ITextBuffer buffer, ITextView view, TestSmartTaggerProvider provider) { m_buffer = buffer; m_view = view; m_provider = provider; m_view.LayoutChanged += OnLayoutChanged; }
Implement GetTags so that the tag is created for the current word. (This method also calls a private method GetSmartTagActions that is explained later.)
Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of TestSmartTag)) Implements ITagger(Of TestSmartTag).GetTags Dim snapshot As ITextSnapshot = m_buffer.CurrentSnapshot If snapshot.Length = 0 Then Return Nothing Exit Function End If 'set up the navigator Dim navigator As ITextStructureNavigator = m_provider.NavigatorService.GetTextStructureNavigator(m_buffer) 'set up a list to contain the tags Dim list As List(Of TagSpan(Of TestSmartTag)) list = New List(Of TagSpan(Of TestSmartTag))() For Each span In spans Dim caret As ITextCaret = m_view.Caret Dim point As SnapshotPoint If CInt(caret.Position.BufferPosition) > 0 Then point = caret.Position.BufferPosition - 1 Else Exit For End If Dim extent As TextExtent = navigator.GetExtentOfWord(point) 'don't display the tag if the extent has whitespace If extent.IsSignificant Then list.Add(New TagSpan(Of TestSmartTag)(extent.Span, New TestSmartTag(GetSmartTagActions(extent.Span)))) Else Exit For End If Next span Return list End Function
public IEnumerable<ITagSpan<TestSmartTag>> GetTags(NormalizedSnapshotSpanCollection spans) { ITextSnapshot snapshot = m_buffer.CurrentSnapshot; if (snapshot.Length == 0) yield break; //don't do anything if the buffer is empty //set up the navigator ITextStructureNavigator navigator = m_provider.NavigatorService.GetTextStructureNavigator(m_buffer); foreach (var span in spans) { ITextCaret caret = m_view.Caret; SnapshotPoint point; if (caret.Position.BufferPosition > 0) point = caret.Position.BufferPosition - 1; else yield break; TextExtent extent = navigator.GetExtentOfWord(point); //don't display the tag if the extent has whitespace if (extent.IsSignificant) yield return new TagSpan<TestSmartTag>(extent.Span, new TestSmartTag(GetSmartTagActions(extent.Span))); else yield break; } }
Add a GetSmartTagActions method to set up the smart tag actions. The actions themselves are implemented in later steps.
Private Function GetSmartTagActions(ByVal span As SnapshotSpan) As ReadOnlyCollection(Of SmartTagActionSet) Dim actionSetList As New List(Of SmartTagActionSet)() Dim actionList As New List(Of ISmartTagAction)() Dim trackingSpan As ITrackingSpan = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive) actionList.Add(New UpperCaseSmartTagAction(trackingSpan)) actionList.Add(New LowerCaseSmartTagAction(trackingSpan)) Dim actionSet As New SmartTagActionSet(actionList.AsReadOnly()) actionSetList.Add(actionSet) Return actionSetList.AsReadOnly() End Function
private ReadOnlyCollection<SmartTagActionSet> GetSmartTagActions(SnapshotSpan span) { List<SmartTagActionSet> actionSetList = new List<SmartTagActionSet>(); List<ISmartTagAction> actionList = new List<ISmartTagAction>(); ITrackingSpan trackingSpan = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive); actionList.Add(new UpperCaseSmartTagAction(trackingSpan)); actionList.Add(new LowerCaseSmartTagAction(trackingSpan)); SmartTagActionSet actionSet = new SmartTagActionSet(actionList.AsReadOnly()); actionSetList.Add(actionSet); return actionSetList.AsReadOnly(); }
Declare the SmartTagsChanged event.
Public Event TagsChanged As EventHandler(Of SnapshotSpanEventArgs) Implements ITagger(Of TestSmartTag).TagsChanged
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
Implement the OnLayoutChanged event handler to raise the TagsChanged event, which causes GetTags to be called.
Private Sub OnLayoutChanged(ByVal sender As Object, ByVal e As TextViewLayoutChangedEventArgs) Dim snapshot As ITextSnapshot = e.NewSnapshot 'don't do anything if this is just a change in case If Not snapshot.GetText().ToLower().Equals(e.OldSnapshot.GetText().ToLower()) Then Dim span As New SnapshotSpan(snapshot, New Span(0, snapshot.Length)) Dim handler As EventHandler(Of SnapshotSpanEventArgs) = Me.TagsChangedEvent If handler IsNot Nothing Then handler(Me, New SnapshotSpanEventArgs(span)) End If End If End Sub
private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { ITextSnapshot snapshot = e.NewSnapshot; //don't do anything if this is just a change in case if (!snapshot.GetText().ToLower().Equals(e.OldSnapshot.GetText().ToLower())) { SnapshotSpan span = new SnapshotSpan(snapshot, new Span(0, snapshot.Length)); EventHandler<SnapshotSpanEventArgs> handler = this.TagsChanged; if (handler != null) { handler(this, new SnapshotSpanEventArgs(span)); } } }
Implement the Dispose method so that it unsubscribes from the LayoutChanged event.
Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) If disposing Then RemoveHandler m_view.LayoutChanged, AddressOf OnLayoutChanged m_view = Nothing End If m_disposed = True End Sub
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!this.m_disposed) { if (disposing) { m_view.LayoutChanged -= OnLayoutChanged; m_view = null; } m_disposed = true; } }
Implementing the Smart Tag Tagger Provider
To implement the smart tag tagger provider
Add a class named TestSmartTagTaggerProvider that inherits from IViewTaggerProvider. Export it with a ContentTypeAttribute of "text", a OrderAttribute of Before="default", and a TagTypeAttribute of SmartTag.
<Export(GetType(IViewTaggerProvider))> <ContentType("text")> <Order(Before:="default")> <TagType(GetType(SmartTag))> Friend Class TestSmartTaggerProvider Implements IViewTaggerProvider
[Export(typeof(IViewTaggerProvider))] [ContentType("text")] [Order(Before = "default")] [TagType(typeof(SmartTag))] internal class TestSmartTaggerProvider : IViewTaggerProvider
Import the ITextStructureNavigatorSelectorService as a property.
<Import(GetType(ITextStructureNavigatorSelectorService))> Friend Property NavigatorService() As ITextStructureNavigatorSelectorService
[Import(typeof(ITextStructureNavigatorSelectorService))] internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
Implement the CreateTagger<T> method.
Public Function CreateTagger(Of T As ITag)(ByVal textView As ITextView, ByVal buffer As ITextBuffer) As ITagger(Of T) Implements IViewTaggerProvider.CreateTagger If buffer Is Nothing OrElse textView Is Nothing Then Return Nothing End If 'make sure we are tagging only the top buffer If buffer Is textView.TextBuffer Then Return New TestSmartTagger(buffer, textView, Me) Else Return Nothing End If End Function
public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag { if (buffer == null || textView == null) { return null; } //make sure we are tagging only the top buffer if (buffer == textView.TextBuffer) { return new TestSmartTagger(buffer, textView, this) as ITagger<T>; } else return null; }
Implementing Smart Tag Actions
To implement smart tag actions
Create two classes, the first named UpperCaseSmartTagAction and the second named LowerCaseSmartTagAction. Both classes implement ISmartTagAction.
Friend Class UpperCaseSmartTagAction Implements ISmartTagAction
internal class UpperCaseSmartTagAction : ISmartTagAction
Friend Class LowerCaseSmartTagAction Implements ISmartTagAction
internal class LowerCaseSmartTagAction : ISmartTagAction
Both classes are alike except that one calls ToUpper and the other calls ToLower. The following steps cover only the uppercase action class, but you must implement both classes. Use the steps for implementing the uppercase action as a pattern for implementing the lowercase action.
Declare a set of private fields.
Private m_span As ITrackingSpan Private m_upper As String Private m_display As String Private m_snapshot As ITextSnapshot
private ITrackingSpan m_span; private string m_upper; private string m_display; private ITextSnapshot m_snapshot;
Add a constructor that sets the fields.
Public Sub New(ByVal span As ITrackingSpan) m_span = span m_snapshot = span.TextBuffer.CurrentSnapshot m_upper = span.GetText(m_snapshot).ToUpper() m_display = "Convert to upper case" End Sub
public UpperCaseSmartTagAction(ITrackingSpan span) { m_span = span; m_snapshot = span.TextBuffer.CurrentSnapshot; m_upper = span.GetText(m_snapshot).ToUpper(); m_display = "Convert to upper case"; }
Implement the properties as follows.
Public ReadOnly Property DisplayText() As String Implements ISmartTagAction.DisplayText Get Return m_display End Get End Property Public ReadOnly Property Icon() As ImageSource Implements ISmartTagAction.Icon Get Return Nothing End Get End Property Public ReadOnly Property IsEnabled() As Boolean Implements ISmartTagAction.IsEnabled Get Return True End Get End Property Private privateSource As ISmartTagSource Public Property Source() As ISmartTagSource Get Return privateSource End Get Private Set(ByVal value As ISmartTagSource) privateSource = value End Set End Property Public ReadOnly Property ActionSets() As ReadOnlyCollection(Of SmartTagActionSet) Implements ISmartTagAction.ActionSets Get Return Nothing End Get End Property
public string DisplayText { get { return m_display; } } public ImageSource Icon { get { return null; } } public bool IsEnabled { get { return true; } } public ISmartTagSource Source { get; private set; } public ReadOnlyCollection<SmartTagActionSet> ActionSets { get { return null; } }
Implement the Invoke method by replacing the text in the span with its uppercase equivalent.
Public Sub Invoke() Implements ISmartTagAction.Invoke m_span.TextBuffer.Replace(m_span.GetSpan(m_snapshot), m_upper) End Sub
public void Invoke() { m_span.TextBuffer.Replace(m_span.GetSpan(m_snapshot), m_upper); }
Building and Testing the Code
To test this code, build the SmartTagTest solution and run it in the experimental instance.
To build and test the SmartTagTest solution
Build the solution.
When you run this project in the debugger, a second instance of Visual Studio is instantiated.
Create a text file and type some text.
A blue line should be displayed under the first letter of the first word of the text.
Move the pointer over the blue line.
A button should be displayed near the pointer.
When you click the button, two suggested actions should be displayed: Convert to upper case and Convert to lower case. If you click the first action, all the text in the current word should be converted to upper case. If you click the second action, all the text should be converted to lower case.
See Also
Tasks
Walkthrough: Linking a Content Type to a File Name Extension