65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (38投票s)

2010 年 3 月 31 日

CPOL

8分钟阅读

viewsIcon

111072

downloadIcon

740

通过调用上下文菜单中的菜单项来扩展 VS,该菜单项允许在代码区域之间移动代码块。

引言

您熟悉 #region 关键字吗?如果熟悉,可以跳过此介绍。

好的,让我们介绍一下 #region 的用途和使用方法。

如果您在一个足够大的类中工作,比如有数百行代码和几十个方法和属性。想象一下,如果这些方法和属性以某种逻辑分组(例如,只读属性、读写属性、static 方法、private 方法、public 方法等),并且这些组是可折叠的。这样可以更轻松、更快速地找到要维护的代码片段。同时,对于使用、维护或审查您代码的其他开发人员来说,也会更容易。

#region 关键字就是为此目的而存在的。有 3 种方法可以在代码中插入和使用 region

  1. 键入 #region 后跟 region 名称,然后在下一行键入 #endregion,然后将您的代码写入或粘贴在两者之间。
  2. 更简单的方法是右键单击,选择 插入代码片段 然后选择 Visual C#/VB.NET ,最后选择 #region
  3. 还有一个额外的选项是选择一个代码块,右键单击,然后选择 环绕,最后选择 #region

背景

要使用当前可用的 Visual Studio 功能通过将代码块分组到逻辑区域来格式化现有代码,您需要执行以下步骤

  1. 创建区域
  2. 对于每个代码块,执行以下操作
    1. 选择该代码块
    2. 剪切选定的代码
    3. 导航到目标区域,或按名称搜索它
    4. 将您的代码粘贴到其中

由于我经常需要执行此操作,而且我认为许多开发人员也需要,所以我创建了 MoveToRegionVSX Visual Studio 扩展,使其更轻松、更快捷。使用此工具,格式化现有代码的过程如下

  1. 创建区域。
  2. 右键单击编辑器并选择“移动到区域”,这将弹出一个工具窗口,其中列出了所有现有区域。
  3. 对于每个代码块,执行以下操作
    1. 选择该代码块。
    2. 在刚打开的工具窗口中双击目标区域。

必备组件

  • Visual Studio 2010 或更高版本
  • Visual Studio 2010 SDK 或更高版本

准备项目

创建 Visual Studio 扩展既有趣又有挑战。第一步是选择正确的项目模板。对于此工具,我使用了 C# 的 Visual Studio Package。在接下来的几行中,我将分步说明如何自己重新创建此工具。

  • 打开 Visual Studio 并选择“新建项目”
  • 选择您的语言(我的选择是 C#)
  • 在模板类别中选择“Extensibility”
  • 在模板中选择“Visual Studio Package”
  • 在向导的前两个屏幕中单击“下一步”(第一个是欢迎界面,第二个是选择语言,该语言已预定义)
  • 在公司名称字段中,我建议输入您的姓名,因为这将用作命名空间名称。另外,为包提供一个描述性的名称(我的名称是 MoveToRegionVSX
  • 下一步非常重要。在“附加功能”屏幕中,同时选择“菜单命令”和“工具窗口”。菜单命令用于将命令添加到上下文菜单,而工具窗口用于启用显示包含 regions 列表的面板。

    MoveToRegionVSX/02.png

  • 输入命令名称“Move to Region”及其命令 ID
  • 输入窗口名称“Select Region”及其命令 ID
  • 在向导的最后一个屏幕中,只需取消选中测试项目。

信不信由你,您已经创建了一个 Visual Studio 2010 的扩展。按 F5 键来证明。这将启动一个新的 Visual Studio 实例。单击“工具”菜单,您将找到一个名为“Move To Region”的新命令,单击它将显示一个消息框。这就是您刚刚创建的扩展的结果。

现在,我们准备进行实际工作了。但首先,让我将任务分解成更小的部分。

  • 将命令从“工具”菜单移动到上下文菜单
  • 创建工具窗口
  • 添加必需的程序集和命名空间
  • 加载区域
  • 将选定的代码移动到选定的区域
  • 处理菜单项单击

将命令从“工具”菜单移动到上下文菜单

MoveToRegionVSX/08.png

由于格式化过程与编辑器相关,我认为我们命令的逻辑位置应该是上下文菜单而不是“工具”菜单。但如您所见,默认使用的菜单是“工具”菜单。在解决方案资源管理器中,打开文件 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,您会看到类似下图的内容

MoveToRegionVSX/ToolWin.png

  • 删除按钮和标签
  • 添加一个列表框并命名为 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 开发的。
如果您需要在较新版本上安装它,只需执行以下操作

  1. 使用您自己的版本打开源代码
  2. 将项目升级到该版本
  3. 更新项目引用以使用您目标版本的 VS SDK

无需更改任何代码。

已在 Visual Studio 2012 和 2013 上进行了测试。
不幸的是,在 2010 年之前的版本中失败了。但由于代码是可用的,如果您需要,可以随意跟踪并更新代码以使其与您的版本兼容。

特别感谢

特别感谢 Matt URon Nicholson 通过 这次有用的讨论给予的支持。

参考文献

不幸的是,用于此目的的程序集没有完全注释。因此,我不得不尝试许多类、接口、方法和属性来完成它。

然而,我从以下 2 个链接中学到了更多

历史

  • 2010 年 3 月 30 日:作为我参与 Visual Studio 2010 扩展竞赛的初始版本提交
  • 2010 年 4 月 1 日:除了文本更改外,还添加了“如何下载”部分
  • 2014 年 4 月 28 日:添加了“支持的 Visual Studio 版本”和“特别感谢”部分
  • 2017 年 1 月 13 日:更新了源代码和插件以支持 Visual Studio 2015
© . All rights reserved.