Introduction
Visual Studio extensions are valuable tools that enhance the day-to-day workflow, keeping the IDE fresh with new functionality, tailored for your work style. We’ve seen fantastic extensions developed by both Microsoft and the community, including several that saw over a million downloads each.
In this tutorial, I’m going to show you how to build a new editor extension for Visual Studio 2012 called MultiEdit. This extension allows you to Alt-Click on various parts of your code and perform simultaneous text writing and deleting. You can find a real, non-tutorial version I wrote on the Visual Studio Gallery, and a demo on what it does right here:
I’ll cover several common development tasks for Editor extensions including:
- Drawing on the editor
- Capturing a certain keyboard and mouse combination
- Editing and inserting new text into the editor in multiple locations, all at once.
I will give you an explicit overview on how to get started with Extensions, and the different pieces that will allow you to build similar experiences. However, implementation elegance and efficiency are not a priority in this tutorial.
BREAKING IT DOWN
Let’s describe a normal use case for this extension. Jill has three loops in a function and wants to change the loop condition for all of them at once. Normally she’ll just go and modify each loop statement separately. With Multi-Point Edit she can edit the conditions simultaneously.
The interaction model will look something like this:
- Jill holds the ALT key, and left clicks on the end of the four loop conditions, one after the other
- Jill presses BACKSPACES until the condition is deleted
- She types the new condition
- Finally, she Left-clicks (without ALT) to exit multi-point edit mode.
There are numerous capabilities we need to harness for this to happen:
- Capture the ALT key being pressed
- Capture the left mouse click
- Draw and redraw the visual aids for the edit points on the editor
- Emulate the tab key, typing, deleting and backspacing
- Locate, save and track the edit points as the view changes (after insertions/deletions)
Starting out
Start by opening Visual Studio, and do the following:
- Select File->New->Project.
- Select the “Extensibility” node in the tree-view
- Select the “Editor Text Adornment” template
- Name the project MultiEdit, and press OK.
(If you can’t find the Extensibility tree item, make sure you have the Visual Studio 2012 SDK installed)
The files currently visible in the Solution Explorer are part of the Editor Text Adornment template which we won’t be using for this tutorial. From Solution Explorer, delete these two files:
- MultiEdit.cs
- MultiEditFactory.cs
Double-click on the source.extension.vsixmanifest, and set the Author property to your liking. This step is required in order to build your extension.
Setting up our Editor Extension
Right-click the project in Solution Explorer and select Add->New Class. Name the file MultiEditCommandFilter.cs, and delete everything from the template.
You will need to add the following reference for the code to work properly:
Microsoft.VisualStudio.OLE.Interop;
Go ahead and copy-paste or type in this code:
using System; using System.Runtime.InteropServices; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Text.Editor; using System.Windows.Input; using System.Collections.Generic; using Microsoft.VisualStudio.Text; using System.Windows.Media; using System.Windows.Controls; namespace MultiEdit { class MultiEditCommandFilter : IOleCommandTarget { } }
MultiEditCommandFilter is a standard class that inherits from IOleCommandTarget, allowing it to communicate with and receive keyboard input from Visual Studio. This allows our extension to capture and provide keystrokes functionality in the editor.
Let’s add a few fields to our class: m_textView (Line 3) will hold a reference to the Visual Studio editor this class will interact with. m_nextTarget (Line 4) will hold the reference to the next command Visual Studio should activate after our extension finishes processing (more on that later). m_adornmentLayer will hold the reference to the adornment layer, or drawing canvas, where all our newly drawn visual aids reside, layered on top of the text editor.
class MultiEditCommandFilter : IOleCommandTarget { private IWpfTextView m_textView; internal IOleCommandTarget m_nextTarget; private IAdornmentLayer m_adornmentLayer;
Let’s dig a little deeper into how the editor is architected. Editors have individual predefined adornment layers:
- Squiggle adornment used to indicate errors
- Textual content of the editor
- The caret
- Selection text caret
- Text markers provided by the ITextMarkerProviderFactory classes
- Collapse hint adornment
Adornments define visual effects that can be added either to the text that is displayed in a text view or to the text view itself.
Adornments derive from UIElement and are graphic elements in the Visual Studio editor, layered on top of one another in a specific z-order. For example, the red squiggle underline used to mark non-compiling code in many programming languages is an adornment. Combining text, shapes, images and other UI elements allows drawing compelling adornments on an adornment layer.
There are two types of editor adornments: Text adornments and viewport adornments.
Viewport adornments focus on placing visuals relative to the editor’s visual surface.
Examples for viewport adornments include:
- Laying out the file owner names in small colorful boxes in the top right corner of a document
- Attaching and displaying comments for specific files
- Drawing a purple rectangle on the top right view of the editor
Text adornments focus on scenarios where the extension inspects portions of the code adding visuals pinned to that text relative area.
Examples for text adornments include:
- Highlighting all occurrences of the selected word
- Coloring every method based on code complexity
- Coloring a specific character occurrence (‘a’) in the editor.
We’ll add a constructor that facilitates getting access from Visual Studio to the text editors we will interact with. Line 11 retrieves the adornment layer we call “MultiEditLayer” and stores it in a private field. We’ll use the layer reference when we’ll want to draw on our editor surface.
class MultiEditCommandFilter : IOleCommandTarget { private IWpfTextView m_textView; internal IOleCommandTarget m_nextTarget; private IAdornmentLayer m_adornmentLayer; public MultiEditCommandFilter(IWpfTextView textView) { m_textView = textView; m_adornmentLayer = m_textView.GetAdornmentLayer("MultiEditLayer"); }
Adornments define visual effects that can be added either to the text that is displayed in a text view or to the text view itself.
Adornments derive from UIElement and are graphic elements in the Visual Studio editor, layered on top of one another in a specific z-order. For example, the red squiggle underline used to mark non-compiling code in many programming languages is an adornment. Combining text, shapes, images and other UI elements allows drawing compelling adornments on an adornment layer.
There are two types of editor adornments: Text adornments and viewport adornments.
Viewport adornments focus on placing visuals relative to the editor’s visual surface.
Examples for viewport adornments include:
- Laying out the file owner names in small colorful boxes in the top right corner of a document
- Attaching and displaying comments for specific files
- Drawing a purple rectangle on the top right view of the editor
Text adornments focus on scenarios where the extension inspects portions of the code adding visuals pinned to that text relative area.
Examples for text adornments include:
- Highlighting all occurrences of the selected word
- Coloring every method based on code complexity
- Coloring a specific character occurrence (‘a’) in the editor.
Implementing the IOleCommandTarget interface, MultiEditCommandFilter becomes a command target, receiving command notifications it can handle.
The next code we’ll add is a method called IOleCommandTarget.Exec() (Line 1). The IDE calls this method to perform our extension’s main functionality. Imagine an infinite loop running in Visual Studio, occasionally calling our extensions’ .Exec() method giving MultiEdit a time slice to operate.
Once the MultiEdit extension finishes it’s operation, we indicate (Line 3) that whoever is next in the loop should get their time slice. It is crucial to minimize the time spent inside the .Exec() method, to keep the IDE as responsive as possible.
int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { return m_nextTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
Add a new method called IOleCommandTarget.QueryStatus() (Line 1). This method reports our command status to Visual Studio and controls whether our command is enabled or disabled for the current IDE state.
int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) { return m_nextTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); }
Keeping things tidy
Our MultiEditCommandFilter.cs should now look like:
using System; using System.Runtime.InteropServices; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Text.Editor; using System.Windows.Input; using System.Collections.Generic; using Microsoft.VisualStudio.Text; using System.Windows.Media; using System.Windows.Controls; namespace MultiEdit { class MultiEditCommandFilter : IOleCommandTarget { private IWpfTextView m_textView; internal IOleCommandTarget m_nextTarget; internal bool m_added; private IAdornmentLayer m_adornmentLayer; public MultiEditCommandFilter(IWpfTextView textView) { m_textView = textView; m_adornmentLayer = m_textView.GetAdornmentLayer("MultiEditLayer"); } int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) { return m_nextTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); } int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { return m_nextTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); } } }
We will need to add several references to the project, several of which we will start leveraging later on in the tutorial. Right-click the References folder in Solution Explorer and choose Add Reference… In the dialog that pops up, locate and make sure the following references are added:
Microsoft.VisualStudio.CoreUtility Microsoft.VisualStudio.Editor Microsoft.VisualStudio.OLE.Interop Microsoft.VisualStudio.Shell.11.0 Microsoft.VisualStudio.Text.Data Microsoft.VisualStudio.Text.Logic Microsoft.VisualStudio.Text.UI Microsoft.VisualStudio.Text.UI.Wpf Microsoft.VisualStudio.TextManager.Interop PresentationCore PresentationFramework System.ComponentModel.Composition
MultiEditFilterProvider.cs
Add another new class to the project, name it MultiEditFilterProvider.cs, removing any text provided by default.
Getting called by Visual Studio needs us to register our class as an extension. We define a new class called MultiEditFilterProvider that inherits from IVsTextViewCreationListener. This class contains metadata for the IDE classifying it as an extension, defining the resources it wants access to (Adornment layer and editor).
IVsTextView contains methods to manage the text view. The view represents the editor window shown in the user interface (UI).
Inheriting from IVsTextViewCreationListener insures our extension registers with Visual Studio only when creating an editor (managed by IVsTextView)
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using Microsoft.VisualStudio; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.Utilities; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.TextManager.Interop; namespace MultiEdit { internal class MultiEditFilterProvider : IVsTextViewCreationListener { } }
Add a new internal field of type AdornmentLayerDefinition and call it m_multieditAdornmentLayer. This field is going to store the adornment layer we’re creating for our extension. We’ll use this layer to draw on the editor.
In order to make this a layer the editor can understand, we’ll need to add some attributes to this field. The first Export attribute is necessary to hook up the MEF extensibility points of this extension.
Next create a Name attribute assigning a name for our adornment layer. Finally, create the TextViewRole attribute to indicate that we’re interested in listening to text views that can be changed using the keyboard.
namespace MultiEdit { internal class MultiEditFilterProvider : IVsTextViewCreationListener { [Export(typeof(AdornmentLayerDefinition))] [Name("MultiEditLayer")] [TextViewRole(PredefinedTextViewRoles.Editable)] internal AdornmentLayerDefinition m_multieditAdornmentLayer = null;
Add another field called editorFactory with the Import attribute (Line 1). This field will reference IVsEditorAdaptersFactoryService providing helper methods by Visual Studio allowing us to get an ITextView view representing the editor text:
[Import(typeof(IVsEditorAdaptersFactoryService))] internal IVsEditorAdaptersFactoryService editorFactory = null;
Our extension is only relevant to editors, and we’d like Visual Studio to assign our extension to every editor created. Every IVsTextView represents an editor window shown in the user interface and by adding a new method called VsTextViewCreated(), we add our extension to every time an editor is created.
The AddCommandFilter on Line 7 assigns an instance of our MultiEditCommandFilter extension class to the created editor:
public void VsTextViewCreated(IVsTextView textViewAdapter) { IWpfTextView textView = editorFactory.GetWpfTextView(textViewAdapter); if (textView == null) return; AddCommandFilter(textViewAdapter, new MultiEditCommandFilter(textView)); }
While there are several other interesting tidbits to dwell into, we’ll keep our focus towards building the Multi Edit Point functionality.
Keeping things tidy
We only want to attach the Command Filter to the View once, and to avoid multiple attachments, we use the internal boolean m_added to indicate whether the Command Filter has been already attached to the editor view.
Our MultiEditFilterProvider.cs file should now look like:
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using Microsoft.VisualStudio; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.Utilities; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.TextManager.Interop; namespace MultiEdit { [Export(typeof(IVsTextViewCreationListener))] [ContentType("text")] [TextViewRole(PredefinedTextViewRoles.Editable)] internal class MultiEditFilterProvider : IVsTextViewCreationListener { [Export(typeof(AdornmentLayerDefinition))] [Name("MultiEditLayer")] [TextViewRole(PredefinedTextViewRoles.Editable)] internal AdornmentLayerDefinition m_multieditAdornmentLayer = null; [Import(typeof(IVsEditorAdaptersFactoryService))] internal IVsEditorAdaptersFactoryService editorFactory = null; public void VsTextViewCreated(IVsTextView textViewAdapter) { IWpfTextView textView = editorFactory.GetWpfTextView(textViewAdapter); if (textView == null) return; AddCommandFilter(textViewAdapter, new MultiEditCommandFilter(textView)); } void AddCommandFilter(IVsTextView viewAdapter, MultiEditCommandFilter commandFilter) { if (commandFilter.m_added == false) { //get the view adapter from the editor factory IOleCommandTarget next; int hr = viewAdapter.AddCommandFilter(commandFilter, out next); if (hr == VSConstants.S_OK) { commandFilter.m_added = true; //you'll need the next target for Exec and QueryStatus if (next != null) commandFilter.m_nextTarget = next; } } } } }
Building the code at this point succeeds, and should give you an idea whether you followed the tutorial steps correctly.
alt left-click
Now let’s interact with the keyboard and mouse and capture the Alt-MouseClick needed for our extension. Switch to the MultiEditCommandFilter.cs window, and add a new Boolean field called requiresHandling as shown on Line 7:
class MultiEditCommandFilter : IOleCommandTarget { private IWpfTextView m_textView; internal IOleCommandTarget m_nextTarget; internal bool m_added; private IAdornmentLayer m_adornmentLayer; private bool requiresHandling = false;
requiresHandling indicates whether our extension should be processing logic during that specific call. For example, consider when the.Exec() method is called in response to a right mouse click; our extension shouldn’t react to that request. requiresHandling should be set to false to avoid any unnecessary processing.
Let’s start off by changing the IOleCommandTarget.Exec() method. We will check the state information passed from Visual Studio, and inspect whether the user left mouse clicked while pressing the ALT key, and if it was, requiresHandling is set to true.
int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { requiresHandling = false; // When Alt Clicking, we need to add Edit points. if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && Keyboard.Modifiers == ModifierKeys.Alt) requiresHandling = true; return m_nextTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); }
Comfort zone #1
Place a break point on the requiresHandling = true; line (Line 7 on the last code box), and debug the extension. When the experimental Visual Studio instance loads, open a new or existing project that has some text in it. Hold ALT and left click on some text within the editor – the debugger will kick in and break.
Pink edit points
So far, we’ve learned how to detect if the left mouse was clicked while alt was pressed, let’s update our progress status:
- Capture the ALT key being pressed
- Capture the left mouse click
- Draw and redraw the visual aids for the edit points on the Editor
- Emulate the tab key, typing, deleting and backspacing
- Locate, save and track the edit points as the view changes (after insertions/deletions)
We’re now going to locate, save and track the edit points we want to synchronize, to do that we need to:
- Detect where the mouse was clicked (while ALT is pressed)
- Save the click location, and track it
One scenario that we need to think about with edit point tracking is that the position of the edit point can move if the user adds text after setting a point. For example, consider a long string of characters (‘aaaaaaabaaaaaaaa’) and an edit point set right after the ‘b’. If we add a few ‘a’s before that edit point, the entire string is pushed forward and our edit point has just moved positions in the editor. Since we still want the edit point to be right after the ‘b’, we’ll need to track the edit point position as text is added. Luckily the editor provides us an easy way to track points even when they change due to character insertions.
Locate, save and track the edit points
Let’s add a few empty functions that we’ll talk about further ahead:
Let’s also add two empty methods called RedrawScreen() and AddSyncPoint(). RedrawScreen will be responsible for redrawing the editor after we draw on it, and AddSyncPoint will be responsible for adding an Edit Point:
private void RedrawScreen() { } private void AddSyncPoint() { }
Next, add Lines 7 to Line 16 to the existing code in IOleCommandTarget.Exec() right under the code we’ve written in the last section (a complete view of the code is available if you scroll down to the next section).
Lines 10 and 11 are identical to Lines 3 and 4 that have been explained in the previous section. Their function is to detect a left-mouse click while the ALT key is pressed.
Once we detect the left-mouse + ALT key, we need add the edit point location to a list, for later retrieval. We then redraw the adornment layer to update the view with all existing edit points.
Note: You will need to add a “}” bracket to the end of the file, to balance the brackets.
requiresHandling = false; // When Alt Clicking, we need to add Edit points. if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && Keyboard.Modifiers == ModifierKeys.Alt) requiresHandling = true; if (requiresHandling == true) { // Capture Alt Left Click, only when the Box Selection mode hasn't been used (After Drag-selecting) if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && Keyboard.Modifiers == ModifierKeys.Alt) { // Add a Edit point, show it Visually AddSyncPoint(); RedrawScreen(); } return m_nextTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); }
Next we’ll look at the AddSyncPoint() method implementation.
Adding an edit point
We first detect a left mouse click inside the editor while the ALT key is pressed. Then we find out where it was clicked, save and track the location in case the editor text changes. Let’s look at the AddSyncPoint() method we’re now defining.
Line 4 gets the text caret position set by the left mouse click. This gives us a position relative to the text placement instead of a pixel position.
Line 5 takes the position of the caret, and builds an ITrackingPoint object (tutorial here) so we can keep track of the location, in case text changes pushes it around. Think of an ITrackingPoint object as a pin placed on text location, if the area moves, the pin moves with it.
Line 6 in the snippet below saves the new ITrackPoint in an ITrackPoint list that we define now as a field in the MultiEditCommandFilter class (2nd snippet below):
private void AddSyncPoint() { // Get the Caret location, and Track it CaretPosition curPosition = m_textView.Caret.Position; var curTrackPoint = m_textView.TextSnapshot.CreateTrackingPoint(curPosition.BufferPosition.Position, Microsoft.VisualStudio.Text.PointTrackingMode.Positive); trackList.Add(curTrackPoint); }
Line 8, introducing the ITrackPoint list:
class MultiEditCommandFilter : IOleCommandTarget { private IWpfTextView m_textView; internal IOleCommandTarget m_nextTarget; internal bool m_added; private IAdornmentLayer m_adornmentLayer; private bool requiresHandling = false; List<ITrackingPoint> trackList = new List<ITrackingPoint>();
Let’s look at the entire file contents, and take call it a ‘Comfort Zone’ moment:
Comfort zone #2
The MultiEditCommandFilter.cs file should now look like:
using System; using System.Runtime.InteropServices; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Text.Editor; using System.Windows.Input; using System.Collections.Generic; using Microsoft.VisualStudio.Text; using System.Windows.Media; using System.Windows.Controls; namespace MultiEdit { class MultiEditCommandFilter : IOleCommandTarget { private IWpfTextView m_textView; internal IOleCommandTarget m_nextTarget; internal bool m_added; private IAdornmentLayer m_adornmentLayer; private bool requiresHandling = false; List<ITrackingPoint> trackList = new List<ITrackingPoint>(); public MultiEditCommandFilter(IWpfTextView textView) { m_textView = textView; m_adornmentLayer = m_textView.GetAdornmentLayer("MultiEditLayer"); } int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) { return m_nextTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); } int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { requiresHandling = false; // When Alt Clicking, we need to add Edit points. if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && Keyboard.Modifiers == ModifierKeys.Alt) requiresHandling = true; if (requiresHandling == true) { // Capture Alt Left Click, only when the Box Selection mode hasn't been used (After Drag-selecting) if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && Keyboard.Modifiers == ModifierKeys.Alt) { // Add a Edit point, show it Visually AddSyncPoint(); RedrawScreen(); } } return m_nextTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); } private void AddSyncPoint() { // Get the Caret location, and Track it CaretPosition curPosition = m_textView.Caret.Position; var curTrackPoint = m_textView.TextSnapshot.CreateTrackingPoint(curPosition.BufferPosition.Position, Microsoft.VisualStudio.Text.PointTrackingMode.Positive); trackList.Add(curTrackPoint); } private void RedrawScreen() { } } }
Set a breakpoint on Line 56, start debugging, load (or create) a project in the newly opened experimental Visual Studio IDE, write some text, hold ALT and left mouse click on some text in the editor. If things worked out right, the debugger should kick in, and you can step through the function lines to see the caret location being fetched and saved.
Next up, let’s draw some stuff so we can SEE things!
Drawing Points
We’ve successfully saved locations we’d like to present as edit point indicators, and now we need to draw them on our adornment layer (called “MultiEditLayer”). In the last section we introduced an empty method called RedrawScreen() for that purpose, now is the time to implement it.
We draw every edit point on the adornment layer by iterating over the tracking point list (trackList), and drawing each point separately. Line 3 removes all previously drawn adornments and Line 6 gets the trackpoint we now want to draw. DrawSingleSyncPoint() will take care of the drawing:
private void RedrawScreen() { m_adornmentLayer.RemoveAllAdornments(); for (int i = 0; i < trackList.Count; i++) { var curTrackPoint = trackList[i]; DrawSingleSyncPoint(curTrackPoint); } }
To visualize an edit point, we’ll draw a small pink rectangle at every Alt-Left-Mouse click position, edit point behavior will mimic a normal caret. To draw on the editor, we’ll need the drawing location and a shape to draw with the coloring properties set.
We’ll now describe the process of drawing a single Sync Point. Follow the line-by-line description of the code shown below:
Line 4 provides the position needed for the graphics object drawing area, and we create it using the point we’ve tracked and passed as an argument to the drawing method.
Line 6 defines the brush and color we’re interested in using for visualizing our edit point.
Line 7 gets the text marker geometry for the specified range of text in the buffer.
Line 8 draws a Geometry using the specified Brush and Pen, and enables us to power Line 9
Line 9 checks whether the point we’re tracking is visible in the current user screen. If drawing.Bounds.IsEmpty is true, then there’s no reason for us to create a visual aid for it. Think of the scenario where the user added an edit point, then scrolled far down, taking that edit point outside the view. Redrawing the screen with the point out of the current screen view, calling drawing.Bounds.IsEmpty returns true.
On Line 12, we create the pink rectangle that we will be showing on the adornment layer.
Line 15 and 16 anchor the rectangle position. Line 17 adds the newly created rectangle to the adornment layer we defined back in the first sections of this tutorial. We set the behavior of its position to TextRelative, pinning the rectangle position to its surrounding text:
private void DrawSingleSyncPoint(ITrackingPoint curTrackPoint) { SnapshotSpan span; span = new SnapshotSpan(curTrackPoint.GetPoint(m_textView.TextSnapshot), 1); var brush = new SolidColorBrush(Colors.LightPink); var g = m_textView.TextViewLines.GetLineMarkerGeometry(span); GeometryDrawing drawing = new GeometryDrawing(brush, null, g); if (drawing.Bounds.IsEmpty) return; System.Windows.Shapes.Rectangle r = new System.Windows.Shapes.Rectangle() { Fill = brush, Width = drawing.Bounds.Width / 2, Height = drawing.Bounds.Height }; Canvas.SetLeft(r, g.Bounds.Left); Canvas.SetTop(r, g.Bounds.Top); m_adornmentLayer.AddAdornment(AdornmentPositioningBehavior.TextRelative, span, "MultiEditLayer", r, null); }
Comfort zone #3
Build the project and start debugging. When the experimental Visual Studio IDE opens up, load (or create) a test project and write some text/code into the Editor. Once you have a couple of lines written, go ahead and ALT-CLICK on different points on the editor where there’s code, you should start seeing the edit points we’ve defined. Try to scroll the page a bit and you’ll see that the edit points stick, but might disappear if you get them out of the view and back (we’ll get to that)
Scrolling fix
To fix the disappearing points when scrolling, lets introduce a quick fix of redrawing the screen when the screen layout changes. First we need to define what happens if and when the screen layout changes, so we’ll introduce a new method:
private void m_textView_LayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { RedrawScreen(); }
Then wire that method to the LayoutChanged event on the textview we’re handling within the class. Registering to events gives us greater control on when to trigger our extension functions. Let’s alter the MultiEditCommandFilter constructor, adding Line 6:
public MultiEditCommandFilter(IWpfTextView textView) { m_textView = textView; m_adorned = false; m_adornmentLayer = m_textView.GetAdornmentLayer("MultiEditLayer"); m_textView.LayoutChanged += m_textView_LayoutChanged; }
You can test out the code and see the effects when scrolling around.
Cancel that
We’ve added the edit points, but now would like to return to normal operation, and we do that by simply left-clicking anywhere without ALT pressed. Clearing out the edit points needs us to clear the adornment layer we’ve been adding visuals to, and return the internal structures to an initial state.
Keeping Visual Studio fast and responsive needs us to minimize processing time when the user left-clicks inside the editor (that’s a lot of clicks). Processing left-clicks without ALT pressed is necessary only when edit points are already placed.
Let’s modify the .Exec() method to process instances where the user clicked the left-mouse and edit points are being tracked:
int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { … // When Left clicking and there are Edit points in trackList, we need to do some cleaning up else if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && trackList.Count > 0) requiresHandling = true; … if (requiresHandling == true) { … else if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.ECMD_LEFTCLICK && trackList.Count > 0) { // Switch back to normal, clear out Edit points ClearSyncPoints(); RedrawScreen(); } …
All we’re missing now is the ClearSyncPoints() method:
private void ClearSyncPoints() { trackList.Clear(); m_adornmentLayer.RemoveAllAdornments(); }
Comfort zone #4
You’re now able to add points, and cancel them out by left mouse clicking without ALT, go ahead and try it.
Syncronized typing
We’ve managed to place edit points and cancel them out as needed, now we’d like to actually allow synchronized typing. When we type ‘a’, we’d like it to show up at every edit point, including the normal caret position. We first need to capture the moment when a character is typed while there are edit points in place.
Let’s go back to our .Exec() method and add a checkup for TYPECHAR which indicates a character was typed. While we’re at it, we’ll also process for BACKSPACE, TAB, or one of the arrow keys:
int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { … // When Edit points exist, and a character is typed, backspace or delete are pressed, we need to process else if (pguidCmdGroup == VSConstants.VSStd2K && trackList.Count > 0 && (nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR || nCmdID == (uint)VSConstants.VSStd2KCmdID.BACKSPACE || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB || nCmdID == (uint)VSConstants.VSStd2KCmdID.UP || nCmdID == (uint)VSConstants.VSStd2KCmdID.DOWN || nCmdID == (uint)VSConstants.VSStd2KCmdID.LEFT || nCmdID == (uint)VSConstants.VSStd2KCmdID.RIGHT )) requiresHandling = true; …
Now that we know we should be processing this entry, let’s add specific logic for the character-was-typed scenario. After detecting a typed character inside the editor (Line 8), we grab that character and save it into typedChar (Line 10). We then send the character to a method called InsertSyncedChar that encapsulates the insertion functionality (Line 11):
int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { … if (requiresHandling == true) { … // Propogate typed character to all edit points else if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR) { var typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn); InsertSyncedChar(typedChar.ToString()); RedrawScreen(); } …
We now want to insert a character at every edit point we’ve tracked. One important realization is that we don’t need to handle the last edit point. Since the text caret is already there, the default behavior of typing will take care of the last edit point. We insert the character to every tracked point, excluding the last one.
To insert text into the editor, we have to create an edit session as shown on Line 5. This signals to the editor that we’re going to alter its content. We then iterate over every tracked point but the last, and use the .Insert method to add the single character inputString to the position defined by the tracking point.
Once we insert the character at all positions, we .Apply and .Dispose to apply our changes and release the resource:
private void InsertSyncedChar(string inputString) { // Avoiding inserting the character for the last edit point, as the Caret is there and // the default IDE behavior will insert the text as expected. ITextEdit edit = m_textView.TextBuffer.CreateEdit(); for (int i = 0; i < trackList.Count - 1; i++) { var curTrackPoint = trackList[i]; edit.Insert(curTrackPoint.GetPosition(m_textView.TextSnapshot), inputString); } edit.Apply(); edit.Dispose(); }
So far we’ve covered:
- Capture the ALT key being pressed
- Capture the left mouse click
- Draw and redraw the visual aids for the edit points on the Editor
- Emulate the tab key, typing, deleting and backspacing
- Locate, save and track the edit points as the view changes (after insertions/deletions)
Let’s move to the last few bits
tab del and backspace
The backspace, delete and TAB functionalities follow the same pattern of check-and-handle. We will present the full source code once we go over the specific handler code for these 3 scenarios, starting with the delete functionality.
Emulating deletes over all the edit points, needs us to remove one character at the edit point position, using the .Delete() method on Line 7:
private void SyncedDelete() { ITextEdit edit = m_textView.TextBuffer.CreateEdit(); for (int i = 0; i < trackList.Count - 1; i++) { var curTrackPoint = trackList[i]; edit.Delete(curTrackPoint.GetPosition(m_textView.TextSnapshot), 1); } edit.Apply(); edit.Dispose(); }
Backspace emulation is very similar to deletion, with the difference being the character removal position: edit point position – 1.
private void SyncedBackSpace() { ITextEdit edit = m_textView.TextBuffer.CreateEdit(); for(int i=0; i < trackList.Count-1; i++) { var curTrackPoint = trackList[i]; edit.Delete(curTrackPoint.GetPosition(m_textView.TextSnapshot) - 1, 1); } edit.Apply(); edit.Dispose(); }
TABing is the simplest of cases, inserting the “t” string into the text buffer, much like we did for typing characters.
putting it all together
We’ll add some error checking, some special cases and stitch everything together: MultiEditCommandFilter.cs and MultiEditFilterProvider.cs (on CodePlex)
I’d love to hear your comments, feedback and questions!