使用 VS 扩展在代码区域之间移动代码块






4.99/5 (38投票s)
通过调用上下文菜单中的菜单项来扩展 VS,该菜单项允许在代码区域之间移动代码块。
引言
您熟悉 #region
关键字吗?如果熟悉,可以跳过此介绍。
好的,让我们介绍一下 #region
的用途和使用方法。
如果您在一个足够大的类中工作,比如有数百行代码和几十个方法和属性。想象一下,如果这些方法和属性以某种逻辑分组(例如,只读属性、读写属性、static
方法、private
方法、public
方法等),并且这些组是可折叠的。这样可以更轻松、更快速地找到要维护的代码片段。同时,对于使用、维护或审查您代码的其他开发人员来说,也会更容易。
#region
关键字就是为此目的而存在的。有 3 种方法可以在代码中插入和使用 region
。
- 键入
#region
后跟region
名称,然后在下一行键入#endregion
,然后将您的代码写入或粘贴在两者之间。 - 更简单的方法是右键单击,选择
插入代码片段
,Visual C#/VB.NET
,最后选择#region
。 - 还有一个额外的选项是选择一个代码块,右键单击,然后选择
环绕
,最后选择#region
。
背景
要使用当前可用的 Visual Studio 功能通过将代码块分组到逻辑区域来格式化现有代码,您需要执行以下步骤
- 创建区域
- 对于每个代码块,执行以下操作
- 选择该代码块
- 剪切选定的代码
- 导航到目标区域,或按名称搜索它
- 将您的代码粘贴到其中
由于我经常需要执行此操作,而且我认为许多开发人员也需要,所以我创建了 MoveToRegionVSX
Visual Studio 扩展,使其更轻松、更快捷。使用此工具,格式化现有代码的过程如下
- 创建区域。
- 右键单击编辑器并选择“移动到区域”,这将弹出一个工具窗口,其中列出了所有现有区域。
- 对于每个代码块,执行以下操作
- 选择该代码块。
- 在刚打开的工具窗口中双击目标区域。
必备组件
- Visual Studio 2010 或更高版本
- Visual Studio 2010 SDK 或更高版本
准备项目
创建 Visual Studio 扩展既有趣又有挑战。第一步是选择正确的项目模板。对于此工具,我使用了 C# 的 Visual Studio Package。在接下来的几行中,我将分步说明如何自己重新创建此工具。
- 打开 Visual Studio 并选择“新建项目”
- 选择您的语言(我的选择是 C#)
- 在模板类别中选择“Extensibility”
- 在模板中选择“Visual Studio Package”
- 在向导的前两个屏幕中单击“下一步”(第一个是欢迎界面,第二个是选择语言,该语言已预定义)
- 在公司名称字段中,我建议输入您的姓名,因为这将用作命名空间名称。另外,为包提供一个描述性的名称(我的名称是
MoveToRegionVSX
) - 下一步非常重要。在“附加功能”屏幕中,同时选择“菜单命令”和“工具窗口”。菜单命令用于将命令添加到上下文菜单,而工具窗口用于启用显示包含
regions
列表的面板。 - 输入命令名称“Move to Region”及其命令 ID
- 输入窗口名称“Select Region”及其命令 ID
- 在向导的最后一个屏幕中,只需取消选中测试项目。
信不信由你,您已经创建了一个 Visual Studio 2010 的扩展。按 F5 键来证明。这将启动一个新的 Visual Studio 实例。单击“工具”菜单,您将找到一个名为“Move To Region”的新命令,单击它将显示一个消息框。这就是您刚刚创建的扩展的结果。
现在,我们准备进行实际工作了。但首先,让我将任务分解成更小的部分。
- 将命令从“工具”菜单移动到上下文菜单
- 创建工具窗口
- 添加必需的程序集和命名空间
- 加载区域
- 将选定的代码移动到选定的区域
- 处理菜单项单击
将命令从“工具”菜单移动到上下文菜单
由于格式化过程与编辑器相关,我认为我们命令的逻辑位置应该是上下文菜单而不是“工具”菜单。但如您所见,默认使用的菜单是“工具”菜单。在解决方案资源管理器中,打开文件 MoveToRegionVSX.vsct 并搜索以下块。
<!-- In this section you can define new menu groups. A menu group is a
container for other menus or buttons (commands); from a visual point of view
you can see the group as the part of a menu contained between two lines. The
parent of a group must be a menu. -->
<Groups>
<Group guid="guidMoveToRegionVSXCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
在该文件的这部分中,我们的命令的父菜单由 Parent
ID 定义。
IDM_VS_MENU_TOOLS
指的是“工具”菜单IDM_VS_CTXT_CODEWIN
指的是上下文菜单
因此,只需将现有的 parent
ID 替换为上下文菜单的 ID,就完成了。
创建工具窗口
在解决方案资源管理器中,打开文件 MyControl.xaml,您会看到类似下图的内容
- 删除按钮和标签
- 添加一个列表框并命名为
lstbxRegions
,它将用于存放区域列表。(请注意,这是一个 WPF 控件,因此没有 ID,而是设置 Name 属性。) - 调整列表框的尺寸,使其与用户控件相同。例如
width=200
和height=300
。
现在,让我们整合到目前为止的内容。换句话说,让我们在单击“Move to Region”命令时显示工具窗口。
为此,请按照以下步骤操作
- 在解决方案资源管理器中,打开文件 "MoveToRegionVSXPackage.cs" 并找到
MenuItemCallback
事件处理程序。顾名思义,这是单击“Move To Region”命令时调用的事件处理程序。 - 将事件处理程序的正文替换为对
ShowToolWindow
方法的简单调用private void MenuItemCallback(object sender, EventArgs e) { //Show the tool window ShowToolWindow(sender, e); }
- 现在运行它以查看结果。
请注意,第一次运行时,您可能会发现工具窗口比列表框稍大,调整一次后,它将在下次运行时保留其尺寸。
添加必需的程序集和命名空间
为以下程序集添加引用
Microsoft.VisualStudio.CoreUtility
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Text.Data
Microsoft.VisualStudio.Text.Logic
Microsoft.VisualStudio.Text.UI
Microsoft.VisualStudio.Text.UI.Wpf
在文件 "MoveToRegionVSXPackage.cs" 中,用以下 using
语句替换现有的语句
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
加载区域
思路很简单。我们需要访问当前活动的 कोड 查看器,然后解析其文本,查找关键字 #region
的所有出现。对于找到的每个 region
,我们将读取其名称并将其添加到 string
列表中,然后用该 string
列表填充工具窗口的列表框。
所以,首先,我们将在 MoveToRegionVSXPackage
类中声明以下成员
public sealed class MoveToRegionVSXPackage : Package
{
//<gouda>
//The current active editor's view info
private IVsTextView currentTextView;
private IVsUserData userData;
private IWpfTextViewHost viewHost;
private string allText;
private const string keyword = "#region ";
//</gouda>
现在,我们可以实现 GetRegions()
方法
/// <summary>
/// Parse all the text in the active editor's view and get all regions
/// </summary>
/// <returns> list of strings containing the names of the existing regions </returns>
internal List<string> GetRegions()
{
List<string> regionsList = new List<string>();
userData = currentTextView as IVsUserData;
if (userData == null)// no text view
{
Console.WriteLine("No text view is currently open");
return regionsList;
}
// In the next 4 statements, I am trying to get access to the editor's view
object holder;
Guid guidViewHost = DefGuidList.guidIWpfTextViewHost;
userData.GetData(ref guidViewHost, out holder);
viewHost = (IWpfTextViewHost)holder;
//Now, I will load all the text of the editor to detect the key word "#region"
allText = viewHost.TextView.TextSnapshot.GetText();
string[] regionDelimitedCode = System.Text.RegularExpressions.Regex.Split(allText,
"\\s#region\\s", //'\s' means any white space character e.g. \t, space, \n, \r, etc
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
for (int index = 1/*skip first block*/; index < regionDelimitedCode.Length; index++)
{
regionsList.Add(regionDelimitedCode[index].Split('\r')[0]);
}
return regionsList;
}
现在,假设我们已经有了 regions
列表,所以我们可以填充放置在工具窗口上的列表框。为此,我在 MyControl
类中添加了一个简单的方法,并将其命名为 PopulateList
。
/// <summary>
/// Populates the list box with the list of regions found in the current active view
/// </summary>
/// <param name="regionsList"> list of strings holding the names of regions </param>
/// <param name="activeView"> reference to my package (MoveToRegion Package)</param>
public void PopulateList(List<string> regionsList, MoveToRegionVSXPackage myPkg)
{
//Here is the best place to initialise the packageRef
//I need that reference to enable calling the method MoveToRegion later on double click
//Unless it is logically to do this initialization in the constructor,
//this cannot be done
//because we do not create instance of that class directly
if(packageRef == null)
packageRef = myPkg;
lstbxRegions.Items.Clear();
foreach (string s in regionsList)
lstbxRegions.Items.Add(s);
}
您可以看到,我们可以使用 MoveToReionVSXPackage
类的成员 GetRegions
来检索所有区域,并使用 MyControl
类的成员 PopulateList
填充列表框。那么,如何将前者方法返回的区域列表作为参数传递给后者方法呢?
这个地方是 ShowToolWindow
方法。为了论证,我们需要了解这个方法是如何工作的。
方法正文由向导生成如下
/// <summary>
/// This function is called when the user clicks the menu item that shows the
/// tool window. See the Initialize method to see how the menu item is associated to
/// this function using the OleMenuCommandService service and the MenuCommand class.
/// </summary>
private void ShowToolWindow(object sender, EventArgs e)
{
// Get the instance number 0 of this tool window. This window is single
// instance so this instance
// is actually the only one.
// The last flag is set to true so that if the tool window does not exists
// it will be created.
ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
if ((null == window) || (null == window.Frame))
{
throw new NotSupportedException(Resources.CanNotCreateWindow);
}
IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}
window
对象是调用 FindToolWindow
方法的结果,该方法将 MyToolWindow
的类型作为第一个参数。此调用在其本身调用 MyToolWindow
类的无参数构造函数 **仅在第一次调用时**。如果您访问该类,您只会发现该构造函数。构造函数又将 MyControl
类的新实例分配给 Content
属性。这就是列表框和菜单命令之间的关系。
因此,在获取窗口后,我将将其 Content
属性转换为 MyControl
以调用 PopulateList
方法。
ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
//<gouda>
List<string> regionsList = this.GetRegions();
//call the method PopulateList which is member in MyControl class
//So, cast the Content property of window to MyControl
((MyControl)window.Content).PopulateList(regionsList, this);
//</gouda>
将选定的代码移动到选定的区域
这里的想法是检测选择,一旦我们初始化了 viewHost
(稍后我会告诉您何时以及如何初始化它),这很容易。
初始化它之后,我们可以访问它的 TextView
属性,该属性提供了丰富的属性和方法的列表。
相反,我将让您查看带有少量注释的代码。
/// <summary>
/// Moves the selected text to the given regionName
/// If no selection, does nothing
/// </summary>
/// <param name="regionName"> The name of the destination region </param>
internal void MoveToRegion(string regionName)
{
if (viewHost.TextView.Selection.IsEmpty)
return;
//Get the selected text
string selectedText = viewHost.TextView.Selection.StreamSelectionSpan.GetText();
//get the selected span to delete its contents
Span deleteSpan = viewHost.TextView.Selection.SelectedSpans[0];
//now, delete the span as its text is saved in the selectedText
viewHost.TextView.TextBuffer.Delete(deleteSpan);
//Now, I will load all the text of the editor again, because it is subject to change
allText = viewHost.TextView.TextSnapshot.GetText();
//get the position at which region exists
string fullRegionName = keyword + regionName;
int regPos = allText.IndexOf(fullRegionName) + fullRegionName.Length;
//insert the selected text at the specified position
viewHost.TextView.TextBuffer.Insert(regPos, "\r" + selectedText);
}
处理菜单项单击
在这里,我们从 currentTextView
初始化,之后我们可以初始化 viewHost
。然后,我们显示工具窗口。
/// <summary>
/// This function is the callback used to execute a command when the a menu item
/// is clicked.
/// See the Initialize method to see how the menu item is associated to this
/// function using
/// the OleMenuCommandService service and the MenuCommand class.
/// </summary>
private void MenuItemCallback(object sender, EventArgs e)
{
IVsTextManager txtMgr = (IVsTextManager)GetService(typeof(SVsTextManager));
int mustHaveFocus = 1;//means true
//initialize the currentTextView
txtMgr.GetActiveView(mustHaveFocus, null, out currentTextView);
//Show the tool window
ShowToolWindow(sender, e);
}
如何下载
您可以在本文顶部的安装程序链接处下载插件。
或者您可以在 VisualStudio Gallery 或 VisualStudio Marketplace 上找到它。
支持的 Visual Studio 版本
目前,我已更新代码和插件以支持 Visual Studio 2015。
此扩展最初是使用 Visual Studio 2010 开发的。
如果您需要在较新版本上安装它,只需执行以下操作
- 使用您自己的版本打开源代码
- 将项目升级到该版本
- 更新项目引用以使用您目标版本的 VS SDK
无需更改任何代码。
已在 Visual Studio 2012 和 2013 上进行了测试。
不幸的是,在 2010 年之前的版本中失败了。但由于代码是可用的,如果您需要,可以随意跟踪并更新代码以使其与您的版本兼容。
特别感谢
特别感谢 Matt U 和 Ron Nicholson 通过 这次有用的讨论给予的支持。
参考文献
不幸的是,用于此目的的程序集没有完全注释。因此,我不得不尝试许多类、接口、方法和属性来完成它。
然而,我从以下 2 个链接中学到了更多
- http://msdn.microsoft.com/en-us/library/dd884850(VS.100).aspx
- http://www.devx.com/VS_2010/Article/44073
历史
- 2010 年 3 月 30 日:作为我参与 Visual Studio 2010 扩展竞赛的初始版本提交
- 2010 年 4 月 1 日:除了文本更改外,还添加了“如何下载”部分
- 2014 年 4 月 28 日:添加了“支持的 Visual Studio 版本”和“特别感谢”部分
- 2017 年 1 月 13 日:更新了源代码和插件以支持 Visual Studio 2015