Contents
Introduction
The bookmarks feature is something that I never liked much in Visual Studio. I never wanted 100’s of bookmarks. I just wanted some shortcuts for the most frequently used lines/sections.
I didn’t find anything similar and even Microsoft didn’t want to add this feature to Visual Studio, so I went ahead and created this Visual Studio extension.
Numbered Bookmarks is a Visual Studio 2010 extension to create and recall bookmarks by using numbers. It allows the user to create 10 numbered bookmarks (starting from 0 to 9).
The user can add or navigate to the particular bookmark by using the same shortcut key. The tool adds a bookmark margin to the Visual Studio editor, next to the scrollbar. Whenever a bookmark
is created, a visual glyph is placed on the bookmark margin. The user can also create/navigate/clear bookmarks from the Numbered Bookmarks menu under the Tools menu.
Using Numbered Bookmarks is relatively easy:
- Create bookmark: Press key combination Ctrl+Alt+Number to create a bookmark, where Number can be any number from 0 to 9.
- Navigate to bookmark: Press key combination Ctrl+Alt+Number to move to the bookmark location.
- Delete a bookmark: Right click on the bookmark, in the bookmark margin.
- Delete all bookmarks: Press key combination Ctrl+Alt+Backspace.
- Information about a bookmark: Mouse hover over the bookmark shows basic information about it, which includes the bookmark number,
file name, line number, and column number.
- How to use information: Mouse hover over the Green colored bookmark in the bookmark margin shows the basic how to.
Prerequisites
For developing Numbered Bookmarks, we need the following applications pre-installed:
Divide and conquer
The task of explaining a full-flexed Visual Studio extension can be very overwhelming depending on the complexity of the extension. Numbered Bookmarks
is though a simple extension but yes it is a little complicated to explain (more so for a beginner). Let us try to break it into smaller tasks and complete them
one by one, resulting in finding our goal. So, what are we waiting for? Let's get started.
Create the basic infrastructure
We can use the Visual Studio Package Wizard to generate the basic infrastructure. So let's go ahead and create the basics; the steps that you need to follow are:
- Go to File->New and in the New Project dialog, select Other Project Types->Extensibility
and select Visual Studio Package in the right pane. Name the package NumberedBookmarks and click OK.
- Click Next on the Welcome page of the Visual Studio Package Wizard. Select Visual C# as your programming language and select
Generate a new key to sign the assembly option and click Next.
- In the next step, provide information about the package. Modify the icon, company name, package name, package version, and package information, and click Next.
- Select the Menu Command option in the next step. It creates a menu entry in the Tools menu with the caption Numbered Bookmarks. Click Next.
- In the next step, change the Command Name to Numbered Bookmarks and Command ID to
cmdIDNumberedBookmarks
and click Next.
- In the Test Project Options page, de-select both Unit Test Project and Integration Test Project and click Finish. Now our basic infrastructure is ready.
Add menus and submenus
Apart from keyboard shortcuts, the user can also use menu options to create, move to, or remove all bookmarks. Let's add the submenus to the Numbered Bookmarks menu created
in the earlier step. Finally our menus should look like this:
First of all, we will add command IDs to PkgCmdIDList.cs, these IDs are used to hook event handlers to the menu entries.
There is already one entry in this class for the Numbered Bookmarks menu. We are not going to use this menu, rather we will add a submenu to the Tools menu, so delete
the entries and create the command IDs for all menu entries. Finally, it looks like:
static class PkgCmdIDList
{
public const uint cmdBookmark0 = 0x0005;
public const uint cmdBookmark1 = 0x0015;
...
public const uint cmdBookmark9 = 0x0095;
public const uint cmdClearBookmarks = 0x0105;
};
Now, before going further to make changes to the VSCT (Visual Studio Command Table) file for more menu options, let's create a bitmap menu strip for the menu icons (each icon of 16x16).
Yes, we have to create a bitmap strip. For creating this bitmap strip, place all your images side by side in a new image and save the final image as a bitmap. Below is a sample
of the bitmap strip which is used for Numbered Bookmarks. Don't forget to add it to Resources.
It's time to get our hands dirty. Open the NumberedBookmarks.vsct file. Scroll to the Bitmaps
section and modify the section as:
<Bitmaps>
<Bitmap guid="guidIcons"
href="Resources\AllIcons.bmp"
usedList="bmpZero, bmpOne, bmpTwo, bmpThree, bmpFour, bmpFive,
bmpSix, bmpSeven, bmpEight, bmpNine, bmpNumbers"/>
</Bitmaps>
In this section, we specify the location of the bitmap strip and also the names of all the images. Now, let's create symbols for all the images.
Scroll to the symbols section, you'll find a GUIDSymbol
entry for guidImages
. By default Visual Studio adds a bitmap strip to the package.
Remove the section and add the following to it:
<GuidSymbol name="guidIcons" value="{1097bc53-206b-4232-a166-1dfe7cdaedf4}" >
<IDSymbol name="bmpZero" value="1" />
<IDSymbol name="bmpOne" value="2" />
...
<IDSymbol name="bmpNine" value="10" />
<IDSymbol name="bmpNumbers" value="11" />
</GuidSymbol>
Notice that the images are numbered from 1 and not from 0, the value portion contains the index of the image in the bitmap strip from the left.
Each menu is contained inside a menu group. This way we have two menu groups (shocked?). Let me try to explain. We are adding a menu Numbered Bookmarks under
the Tools menu and another sub menu beneath it. So we have two menus, Numbered Bookmarks menu and submenu. Both of these should have corresponding menu groups too. Let's name
them MyMenuGroup
and SubMenuGroup
, respectively. All menu options are added to SubMenuGroup
finally.
We will divide this step in five parts:
- First of all, let's add GUID symbols for all our commands (created earlier) and menus and menu groups. Make sure that all the values match
the command IDs created earlier, otherwise event handlers will not be fired.
<GuidSymbol name="guidNumberedBookmarksCmdSet"
value="{c74fc9bd-32e1-4135-bddd-779021cc3630}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="SubMenuGroup" value="0x1150"/>
<IDSymbol name="SubMenu" value="0x1100"/>
<IDSymbol name="cmdBookmark0" value="0x0005"/>
...
<IDSymbol name="cmdBookmark9" value="0x0095"/>
<IDSymbol name="cmdClearBookmarks" value="0x0105"/>
</GuidSymbol>
Secondly, add a Groups
section and add entries for both the groups (as explained earlier). IDM_VS_MENU_TOOLS
is a constant for the Tools menu.
<Groups>
<Group guid="guidNumberedBookmarksCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
<Group guid="guidNumberedBookmarksCmdSet" id="SubMenuGroup" priority="0x0000">
<Parent guid="guidNumberedBookmarksCmdSet" id="SubMenu"/>
</Group>
</Groups>
Thirdly, add a Menus
section and add a menu entry for Numbered Bookmarks. While adding a menu, you can provide a command name and caption to display.
Yes, you cannot provide an icon for a submenu. Sigh!
<Menus>
<Menu guid="guidNumberedBookmarksCmdSet" id="SubMenu"
priority="0x0100" type="Menu">
<Parent guid="guidNumberedBookmarksCmdSet" id="MyMenuGroup"/>
<Strings>
<ButtonText>Numbered Bookmarks</ButtonText>
<CommandName>Numbered Bookmarks</CommandName>
</Strings>
</Menu>
</Menus>
Fourthly, let's add our sub menu entries to the file now. In order to add a menu, we use a Button
tag inside a Buttons
tag. We can provide the command name, caption,
and icon for each of the menus. Other entries can also be added similar to the sample below:
<Button guid="guidNumberedBookmarksCmdSet" id="cmdBookmark1"
priority="0x0000" type="Button">
<Parent guid="guidNumberedBookmarksCmdSet" id="SubMenuGroup" />
<Icon guid="guidIcons" id="bmpOne" />
<Strings>
<CommandName>cmdBookmark1</CommandName>
<ButtonText>Bookmark 1</ButtonText>
</Strings>
</Button>
Last, let's create keyboard shortcuts for our menus. This is known as key-binding. While adding a key binding, you can provide two modifiers and two keys.
Strange again. Let's try to recall the key combination for Solution Explorer is (Ctrl+W, S), which can be divided in two parts. First being Ctrl+W and second being S. Ctrl is modifier1
and W is key1, similarly S is key2. The modifier can be a combination of Ctrl, Alt, or Shift separated by a space.
<KeyBindings>
<KeyBinding guid="guidNumberedBookmarksCmdSet" id="cmdBookmark0"
editor="guidVSStd97" key1="0" mod1="Control Alt" />
<KeyBinding guid="guidNumberedBookmarksCmdSet" id="cmdClearBookmarks"
editor="guidVSStd97" key1="VK_BACK" mod1="Control Alt" />
</KeyBindings>
Finally, we reach the place where we can bind our commands to the event handlers. In the Initialize
function of the package class (in this case,
NumberedBookmarksPackage
), we can bind the command with an event handler and in the same class, we can create an event handler for the same.
CommandID menuCommandID = new CommandID(GuidList.guidNumberedBookmarksCmdSet,
(int)PkgCmdIDList.cmdIdNumberedBookmarks);
MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID );
mcs.AddCommand( menuItem );
private void MenuItemCallback(object sender, EventArgs e) { }
In the case of Numbered Bookmarks, the functionality of adding bookmarks is the same for all 10 entries (numbers 0 to 9) and in the event handler, we call a function
AddBookmark
with an int
parameter to specify which bookmark to add. So, I opted for anonymous methods in place of event handlers.
CommandID menuCommandBookmark0 = new CommandID(
GuidList.guidNumberedBookmarksCmdSet,
(int)PkgCmdIDList.cmdBookmark0);
MenuCommand subItemBookmark0 = new MenuCommand(
new EventHandler(
delegate(object sender, EventArgs args)
{ AddOrMoveToBookmark(0); }),
menuCommandBookmark0);
mcs.AddCommand(subItemBookmark0);
CommandID menuCommandClearBookmark = new CommandID(
GuidList.guidNumberedBookmarksCmdSet,
(int)PkgCmdIDList.cmdClearBookmarks);
MenuCommand subItemClearBookmark = new MenuCommand(
new EventHandler(
delegate(object sender, EventArgs args)
{ ClearAllBookmarks(); }),
menuCommandClearBookmark);
mcs.AddCommand(subItemClearBookmark);
private void ClearAllBookmarks() { }
private void AddOrMoveToBookmark(int bookmarkNumber) { }
Create bookmark margin
Creating a margin or bookmark margin is a twofold process. We need to create two classes, the first one inherited from IWpfTextViewMarginProvider
, and the second
one inherited from IWpfTextViewMargin
. We need to export our factory (provider) class to MEF. As per the MEF philosophy 'You export, we import. We export, you import.'
Here we export our provider class and MEF imports it, and this class tells MEF how to create a margin. The second class actually creates the margin.
We have inherited it from Border
to provide some basic WPF functionality like background color, width, height, etc.
We could have opted for some other control or a custom control as well.
[Export(typeof(IWpfTextViewMarginProvider))]
[Name(BookmarkMargin.MarginName)]
[Order]
[MarginContainer(PredefinedMarginNames.Right)]
[ContentType("code")]
[TextViewRole(PredefinedTextViewRoles.Document)]
internal sealed class MarginFactory : IWpfTextViewMarginProvider
{
public IWpfTextViewMargin CreateMargin(IWpfTextViewHost textViewHost,
IWpfTextViewMargin containerMargin)
{
return new BookmarkMargin(textViewHost.TextView, bookmarkManager);
}
}
Let's try to understand all the attributes applied to the MarginFactory
class:
Export
: Tells what type of export this class provides, which is IWpfTextViewMarginProvider
in our case.
Name
: Tells the name of the export provided, which is BookmarkMargin.MarginName
(a constant) in our case.
Order
: Tells MEF to order/arrange multiple instances of the extension.
MarginContainer
: This attribute tells the name of the container (pre-defined constant), in our case it is PredefinedMarginNames.Right
.
Other options can be Left
, Right
, Top
, Bottom
, ScrollBar
, ZoomControl
, LineNumber
,
Spacer
, Selection
, Glyph
, etc.
ContentType
: Declares an association of the extension with a particular type of content, which is code in our case.
TextViewRole
: Specifies what type of view the extension should be associated with, in our case it is Document
. Other options can be Editable
,
Debuggable
, Zoomable
, etc.
class BookmarkMargin : Border, IWpfTextViewMargin
{
...
}
IWpfTextViewMargin
represents a margin which is attached to an edge of the Visual Studio Editor (IWPFTextView
).
Create bookmark
The bookmark is a very simple WPF custom control with an ellipse added to it. To improve the overall look and feel of the bookmark (called glyph), we polished
it by adding a few resources. We also created a style for the tooltip, which is shown when the mouse moves over the bookmark.
<UserControl ...>
<Canvas>
<Ellipse Stroke="OrangeRed" Height="16" Width="16"
x:Name="ellipse" Canvas.Left="0" Canvas.Top="0">
<Ellipse.Fill>
<RadialGradientBrush
GradientOrigin="0.25, 0.15">
<GradientStop Color="Orange" Offset="0.2"/>
<GradientStop Color="OrangeRed" Offset="0.9"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Canvas>
</UserControl>
<Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="HasDropShadow" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border Name="Border"
Background="{StaticResource LightBrush}"
BorderBrush="{StaticResource SolidBorderBrush}"
BorderThickness="1"
CornerRadius="5"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<ContentPresenter
Margin="4"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="HasDropShadow" Value="true">
<Setter TargetName="Border"
Property="CornerRadius" Value="5"/>
<Setter TargetName="Border"
Property="SnapsToDevicePixels" Value="true"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Collect file, line, and column information
In order to get the currently opened document, line number, and column number, we will use the DTE2
object. The DTE2
object represents the Visual Studio .NET IDE
and is the top-most object in the automation model hierarchy. Let's get an instance of DTE2
:
private DTE2 GetDTE2()
{
DTE dte = (DTE)GetService(typeof(DTE));
DTE2 dte2 = dte as DTE2;
if (dte2 == null)
{
return null;
}
return dte2;
}
We can get an instance of the Visual Studio automation model (DTE) using the GetService
function (remember, it is part of the Package
class and our
package is inherited from Package
). We need to type cast the DTE object to a DTE2
object. Due to some historical reasons (this is how MSDN explains it),
we have to perform these two operations, get DTE object first and then convert it to DTE2
. We can get the name of the opened file using
the ActiveDocument
property of the DTE2
object.
For line and column number, we need to extract the VirtualPoint
object (current cursor location) from the ActiveDocument.Selection.ActivePoint
property
of the DTE2 object. Both line and column number starts from 1 (again historical reasons, because it came from the VB scripting world).
string documentName = GetDTE2().ActiveDocument.Name;
VirtualPoint point = GetDTE2().ActiveDocument.Selection.ActivePoint;
int lineNumber = point.Line;
int columnNumber = point.DisplayColumn;
Calculate bookmark positions
Calculating bookmark positions is fairly simple but what do we exactly mean with bookmark position? Actually, when the user opts for creating a bookmark at a particular
position, we want to create a bookmark in the margin in a relative place. Let us assume that the line number is 5 and the total number of lines in the document is 20, then
we want our bookmark to be placed somewhere around one-fourth (1/4th) the height of the margin. For calculating the position, we need the total number of lines in the document and
the height of the window (which is the ViewPort). We can get both these things from IWpfTextView
. Now the question comes, how to get the IWpfTextView
instance.
Simple, it can be retrieved from the TextView
property of IWpfTextViewHost
. Now the question is how to get
IWpfTextViewHost
. Get the instance of SVsTextManager
using the GetService
function; extract its ActiveView
as IVsTextView
.
Cast IVsTextView
as IVsUserData
and get an IWpfTextViewHost
instance by calling the GetData
function of IVsUserData
with
the DefGuidList.guidIWpfTextViewHost
pre-defined constant. Isn't it simple?
private IWpfTextViewHost GetIWpfTextViewHost()
{
IVsTextManager txtMgr = (IVsTextManager)GetService(typeof(SVsTextManager));
IVsTextView vTextView = null;
int mustHaveFocus = 1;
txtMgr.GetActiveView(mustHaveFocus, null, out vTextView);
IVsUserData userData = vTextView as IVsUserData;
if (userData == null)
{
Trace.WriteLine("No text view is currently open");
return null;
}
IWpfTextViewHost viewHost;
object holder;
Guid guidViewHost = DefGuidList.guidIWpfTextViewHost;
userData.GetData(ref guidViewHost, out holder);
viewHost = (IWpfTextViewHost)holder;
return viewHost;
}
As we have the IWpfTextViewHost
now, let's calculate the Y position of the bookmark. Take the line count and viewport height from
the TextSnapshot
from TextView
and calculate the position.
private double GetYCoordinateFromLineNumber(int lineNumber)
{
int totalLines = this.textView.TextSnapshot.LineCount;
double ratio = (double)lineNumber / (double)totalLines;
double yPos = ratio * textView.ViewportHeight;
return Math.Ceiling(yPos);
}
So we have the position now; let's talk about some complexities in calculating and positioning the bookmark on the margin. What if:
- Position is less than zero or greater than ViewPortHeight: Fix it by adding or subtracting a few pixels, as simple as that.
private double AdjustYCoordinateForBoundaries(double position)
{
double currentPosition = position;
double viewPortHeight = Math.Ceiling(textView.ViewportHeight);
return currentPosition;
}
We already have another bookmark at the same position: In this case, try to find the next available position for the bookmark. Again, if we have
another bookmark at that position, then? Then find the next available position. Overall, what Numbered Bookmarks does in this case is, it keeps on searching for the next available
location towards the bottom and if it is still not able to find any available location, then it starts finding a position towards the top of the window and this way, it makes calculations
for the bookmark position. Another question is how to check if a bookmark is already there at that position? We iterate through all the elements in the bookmark margin
and compare the y-coordinate with the y-position.
private double AdjustYCoordinateForExistingBookmarks(double position)
{
return FindNextAvailableYCoordinate(position, 1);
}
public double FindNextAvailableYCoordinate(double position, int multiplier)
{
double currentPosition = position;
foreach (UIElement item in marginCanvas.Children)
{
double topOfThisElement = Canvas.GetTop(item);
if (Math.Abs(currentPosition - topOfThisElement) < BookmarkManager.BookmarkGlyphSize)
{
}
}
return currentPosition;
}
Manage all bookmarks
Before coming to all bookmarks, let's talk a little bit about the bookmark. We created a Bookmark
class that represents a bookmark. It is a very simple class with public
properties for Number
(bookmark number), LineNumber
, ColumnNumber
, and FileName
, and two overloaded constructors to initialize the variables.
For managing all the bookmarks, we created a BookmarkManager
class. This class keeps a dictionary (bookmark number is the key and
the object of the Bookmark
class is the value) of all bookmarks which are accessible through the public property Bookmarks
. It also declares an event
BookmarksUpdated
, which gets fired whenever any changes to the dictionary are made. Why this? Simply to update the margin whenever another bookmark is added or removed.
It also provides the functionality to go to a particular bookmark number (location stored with it). We simply find the ProjectItem
using
DTE2
and the FileName
stored in the bookmark. Open the document and activate it and then move, move cursor to the start of the document,
and then move to the offset provided by LineNumber
and ColumnNumber
.
public void GotoBookmark(int position)
{
Bookmark bookmark = Bookmarks[position];
EnvDTE.ProjectItem document = dte2.Solution.FindProjectItem(bookmark.FileName);
document.Open(BookmarkMargin.vsViewKindCode).Activate();
EnvDTE.TextSelection selection = dte2.ActiveDocument.Selection;
selection.StartOfDocument();
selection.MoveToLineAndOffset(bookmark.LineNumber, bookmark.ColumnNumber);
}
The AddBookmark
and RemoveBookmark
functions simply add an entry to the Bookmarks dictionary and fire the event.
BookmarkGlyph
(our WPF custom control) subscribes to the MouseLeftButtonDown
and MouseRightButtonDown
events and calls GoToBookmark
and
RemoveBookmark
of the bookmark manager to provide the functionality.
Add bookmarks and handle events
The BookmarkMargin
class handles updating the margin by adding or removing bookmarks to it. Basically, this class inherits from
IWpfTextViewMargin
and the Border
class. We add a Canvas
to its children. While adding bookmarks, we add them
to the Canvas
object. The BookmarkMargin
class holds an instance of BookmarkManager
. In the constructor
of the margin, we call the UpdateBookmarks
function which in turn calls UpdateBookmark
for each bookmark. We remove all the children from
the Canvas
and then create all the bookmarks and add them to the children.
private void UpdateBookmarks()
{
if (marginCanvas.Children.Count > 0)
{
marginCanvas.Children.Clear();
}
if (bookmarkManager != null)
{
foreach (Bookmark bookmark in bookmarkManager.Bookmarks.Values)
{
UpdateBookmark(bookmark);
}
}
}
private void UpdateBookmark(Bookmark bookmark)
{
double yPos = GetYCoordinateForBookmark(bookmark);
yPos = AdjustYCoordinateForBoundaries(yPos);
yPos = AdjustYCoordinateForExistingBookmarks(yPos);
BookmarkGlyph glyph;
if (bookmark.Number != BookmarkManager.HelpBookmarkNumber)
{
glyph = CreateBookmarkGlyph(bookmark, yPos);
}
else
{
glyph = CreateHelpGlyph(bookmark);
}
marginCanvas.Children.Add(glyph);
}
CreateBookmarkGlyph
and CreateHelpGlyph
create an instance of the BookmarkGlyph
class with specific properties and return it.
Add help bookmark
Help bookmark is just like any other bookmark with some default/fixed values associated with it. Its bookmark number is 99 (why? because I love it),
and it is always placed in the middle of the bookmark margin. While creating a BookmarkGlyph
object, we identify the bookmark and change its properties like
we change the text to question mark, its fill color to a green colored gradient, and provide a different tooltip than a regular bookmark (not providing the code as it is very straightforward).
Export package and bookmark margin
This is one of the most important steps of creating a package. If we fail to specify the content of the package, then it won't work as specified (bookmark margin
will not be created). A most important point to note here is the Description
field. If this field is more than 280 characters, then you can't upload it to
the Visual Studio Gallery. You can also provide two images in the manifest, which will be used by the Visual Studio Gallery to show with it.
Let's come back to the content part. Click on the Add Content button and select MEF Component as the content type
and choose Project as a source and select NumberedBookmarks from the dropdown.
Make sure that both VS Package and MEF Component are added to the contents list.
Bind it up
Let's try to understand the system as a whole. MarginFactory
creates a BookmarkMargin
and also associates a BookmarkManager
to this particular instance
of BookmarkMargin
. BookmarkMargin
creates BookmarkGlyph
and adds bookmark glyphs to the margin instance.
BookmarkManager
in place keeps a list of all the bookmarks and fires the BookmarksUpdated
event when a bookmark is added or removed.
BookmarkGlyph
in place keeps a track of the associated BookmarkManager
and calls its functions for handling mouse down events. Simple!
Give it a dry run
It's time to give it a try. Build and start the solution (with or without debugging), this starts an instance of Visual Studio which is specially meant for testing
extensions, known as Experimental Instance. Extensions can be debugged in this instance.
Download and installation
The extension can be installed in the following ways:
- Build and install: Build the attached solution and double click NumberedBookmarks.vsix in the Release/Debug folder. This will start the installation
of the extension. This is usually a single click installation.
- Use Extension Manager: Go to Tools->Extension Manager. Click Online Gallery in the left navigation panel and type Numbered Bookmarks
in the Search Online Gallery text box and click Enter. It will show the extension with a Download button next to it. Click Download and follow the steps
to install the extension.
- Download from VS Gallery: Download the extension from this
URL and double click the vsix file downloaded (alternatively, you can choose to run the application).
You can uninstall the extension from Extension Manager, by clicking the Uninstall button next to the extension. Restart Visual Studio for the changes to take effect.
Summary
Creating extensions for Visual Studio is pretty simple and straightforward (not always) but this can be tricky sometimes. Numbered Bookmarks is my first attempt
in this direction (specially for VS 2010). Please provide your feedback and suggestions. Don't forget to download and rate my extension on Visual Studio Gallery.
History
- Feb 25, 2010: Initial draft.