前言
在可扩展性开发(六)中,我介绍了对于Solution Explorer的基本操作。不过,对咱们开发人员来说,绝大部分时间面对的还是编辑器。
VS2008的编辑器功能已经颇为强大了,如果我们能熟练使用快捷键,编写代码的过程是相当舒服的,就像《卓有成效的程序员》中所说:
“编程时始终优先使用键盘而非鼠标”
但问题在于,VS面向的是所有的开发人员群体,它只能够提供最通用的功能,如果对VS的编辑器有些额外的需求,我们只好自己动手了,本文将介绍如何扩展文本编辑器。
AOM中编辑器相关的接口
跟以前一样,这里首先简单介绍一下AOM中的相关接口、类型。
1)Documents
在默认情况下,VS会以标签式文档呈现打开的各个文档。这些文档的集合在AOM中就是Documents,它实现了IEnumerable接口。通过该接口,我们可以获取当前打开的文档,它的重要属性和方法有:
- Count:打开文档的数目;
- Add():向集合中添加新的文档;
- CloseAll():关闭所有文档,它的参数为vsSaveChanges枚举,可提供关闭时的行为选项,比如提示用户进行保存;
- Item():根据索引获取集合中的某个文档;
- Open():打开一个文档;
- SaveAll():保存所有文档。
这些成员的含义是相当简单、直白的。我们可以通过循环变量所有打开的文档,以获取所有文档的信息,对于单个文档来说,它对应于Document接口。
2)Document
表示在VS中打开进行编辑的文档。它的成员较多,这里仅介绍一下比较重要的几个:
- FullName/Path/Name:文档的全名、所在目录、文件名;
- Language:文档的语言类型,如CSharp;
- ProjectItem:获取与文档关联的ProjectItem对象;
- Selection:文档中的选定内容;
- Type:文档的类型;
- Activate():将焦点移至该文档;
- Close():关闭文档;
- Redo()/Undo():执行Redo/Undo操作;
- Save():保存文档。
关于Document成员的详细信息,请参看这里。其中的Selection属性非常有用,因为很多时候我们都是先选中文档的部分内容,再进行相应的操作。另外,在打开的多个文档中,只有一个处于活动状态,可以使用DTE.ActiveDocument属性来快速获取该文档。
在获取文档的引用后,下一步就可以考虑如何进行编辑了。我们得了解5个接口:TextSelection、TextPoint、EditPoint、VirtualPoint、TextDocument。相信在了解了这些接口后,你在操作编辑器时会得心应手的。
3)TextSelection
该接口提供对文档的编辑操作和选定文本的访问。它的成员比Document还有多很多,功能非常全面,应当可以满足绝大部分需要了,这里就不再一一列举了,可以参看MSDN的内容。
我们在手工输入代码时,可以看作总是在光标处输入,也可以把光标看作一个点,这个点包含一些信息,如行号、列号等,这样VS就可以处理输入的内容,在Add-In中以编程方式输入时与此类似,这个“点”就是TextPoint。
4)TextPoint
该接口表示文档中的某个位置,EditPoint和VirtualPoint继承于此。它的主要属性和方法有:
AbsoluteCharOffset:从文档开始计算的绝对字符位置,从1开始;
AtEndOfDocument/AtEndOfLine:指示该点是否处于文档/行的结尾;
AtStartOfDocument/AtStartOfLine:指示该点是否处于文档/行的开头;
DisplayColumn:显示列号;
Line:行号;
LineCharOffset:该点在行内的位置;
LineLength:该点所在行的字符数;
CreateEditPoint():创建一个EditPoint对象以对文档进行编辑;
EqualTo()/GreaterThan()/LessThan():与另一个TextPoint比较相互的位置关系;
关于TextPoint的所有成员信息,请参看这里。光有TextPoint还不能编辑,要真正进行编辑,得使用EditPoint接口。
5)EditPoint
EditPoint从TextPoint那里继承了所有的属性和方法,它还提供了很多用于编辑的属性和方法,比如常见的插入、删除、剪切、粘帖、书签操作,还有位置的移动等等,使我们在编辑文本时拥有了强大的能力。关于EditPoint的所有成员信息,请参看这里。
有时一行内的字符数很多,此时在屏幕能就看不到了,也就是说超出了文档的右边距,要操作在右边距之外的文本需要VirtualPoint。
6)VirtualPoint
VirtualPoint也继承自TextPoint,只是添加了少数几个属性和方法,这里就不再赘述了,可以参看这里。
7)TextDocument
最后一个接口是TextDocument,它表示在编辑器中打开的文档。在你了解了前面几个接口的成员后,对TextDocument的成员也很容易了解了。
在操作文本时,大部分时候可以选择从TextSelection开始,不过在某些情况下TextDocument是个不错的开始,可以考虑先使用TextDocument,如果不能满足需要,再转向前面的几个接口。
在介绍了这么多接口之后,该看一个例子了。
CodeTemplate示例
0)问题分析
这一次要给NEnhancer添加的功能是代码模板。它源自我当前的项目需要,项目要求每次修改代码都要添加这样的注释:
//-------------Change Begin------------------------------------
//-----------Code change log for Item 1001 -------------------------------
//-----------Modified by : Anders Cui Change Date: 03/30/2009
//-----------Changes Begin------------------------------------------------------
Console.WriteLine("Hello, World");
//-----------Changes End for Item 1001 ----------------------------------
虽然我不喜欢,但是没办法,还是要一次次地添加注释。一旦在重复地做着什么事情,我就想有什么更好的办法可以替代。
先来分析下这段注释。由于是维护项目,每次改动都对应着客户发过来的一个需求项,Item 1001表示需求项的Id,中间还有代码编写者和日期,另外还要把选中代码包含起来。起初我考虑使用Code Snippet,但有两个不方便的地方:一是对于新的需求,都得更新CodeSnippet;二是不能自动生成日期。
这两个地方在Add-In中都不是问题。可以建立这样的模板:
//-------------Change Begin------------------------------------
//-----------Code change log for -------------------------------
//-----------Modified by : Change Date:
//-----------Changes Begin------------------------------------------------------
//-----------Changes End for ----------------------------------
我们把一些变化的文本提取出来,即
、
,作为参数进行配置,供当前这个模板使用;而对于日期,可将
作为“内置”或“全局”的参数,每个模板都可以使用。在使用Add-In插入代码模板时,只要将各个参数的信息替换到模板中,然后插入到文档中即可。根据这样的思路,可以建立如下的模板文件:
在节点builtInParams下,可以定义多个“全局”参数,应用于多个模板,对于日期类型的参数来说,可以指定格式;每个template节点定义了一个模板,模板可以有自己的参数; 参数比较特别,其作用是指示选中文件所放的位置;最后,约定所有的参数都放在两个“$”中间。
下面来看看如何实现上面的思路。
1)添加命令
在添加命令前,看看NEnhancer当前的代码,里面有很多代码是比较通用的,每次开发Add-In都可以使用,所以把它提取出来放到DTEHelper类中。
在以前添加命令时,往往使用这样的代码:
// Add command
Command command = commands.AddNamedCommand2(addin, cmdName, buttonText, toolTip,
useMsoButton, iconIndex, ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled,
(int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
if (command != null && cmdBar != null)
{
command.AddControl(cmdBar, position);
}
然后要在QueryStatus和Exec方法中添加代码来实现该命令,这样可以每次添加一个菜单项。这对于当前的例子就不太合适了,因为模板可以是多个,即使根据配置文件动态添加这些菜单项,如果模板多了,菜单就会变得很长。一个解决方案是添加嵌套菜单,将所有这些模板对应的菜单放在一个子菜单下。这里介绍添加菜单的另一种方式:
// 向命令栏添加一个弹出菜单
int templatePopupIndex = codeWinCommandBar.Controls.Count + 1;
CommandBarPopup codeTemplatePopup = codeWinCommandBar.Controls.Add(
MsoControlType.msoControlPopup, Type.Missing, Type.Missing,
templatePopupIndex, true) as CommandBarPopup;
codeTemplatePopup.Caption = "Code Template";
// 向弹出菜单添加菜单项
CommandBarButton codeTemplateCmd = helper.AddButtonToPopup(codeTemplatePopup,
codeTemplatePopup.Controls.Count + 1, "Code Template 1", "Code Template 1");
codeTemplateCmdEvent =
_applicationObject.Events.get_CommandBarEvents(codeTemplateCmd) as CommandBarEvents;
codeTemplateCmdEvent.Click +=
new _dispCommandBarControlEvents_ClickEventHandler(CodeTemplateCmdEvent_Click);
添加一个普通的菜单项,也就是添加一个CommandBarButton类型的控件,以这种方式添加菜单项时,我们又看到熟悉的Event.Click += ...了。
在CodeTemplate例子中,我们可以在Add-In运行时读取配置文件,根据模板的个数生成相应个数的菜单项。界面看起来差不多是这样的:
菜单项的标题是配置文件中模板的名字,并且所有这些菜单项使用同一个处理函数:
private void codeTemplateCmdEvent_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
{
CommandBarControl ctrl = CommandBarControl as CommandBarControl;
string content = CodeTemplateManager.Instance.GetTemplateContent(ctrl.Caption);
TextSelection selected = _applicationObject.ActiveDocument.Selection as TextSelection;
selected.Text = content.Replace(CodeTemplateManager.Instance.SELECTED_PARAM, selected.Text);
}
CodeTemplateManager是用于处理代码模板逻辑的类,通过其GetTemplateContent方法可以当前菜单项对应的模板内容。接着通过TextSelection获取活动文档的选定文本,将这些文本替换为模板的内容。(CodeTemplateManager的代码可以在文章结尾下载的代码中看到)
SELECTED_PARAM也就是前面提到的 参数,事实上,到这里除此参数之外的参数都已置换完毕,所以可将当前选中的文本放入作为代码模板的最终内容。这样做是可以将代码插入了,不过速度有些慢,我也没找到原因,不过倒可以借此机会换一种方式来演示其它API的使用:
private void codeTemplateCmdEvent_Click(object CommandBarControl, ref bool Handled, ref bool CancelDefault)
{
CommandBarControl ctrl = CommandBarControl as CommandBarControl;
string content = CodeTemplateManager.Instance.GetTemplateContent(ctrl.Caption);
int indexOfSelectedParam = CodeTemplateManager.Instance.IndexOfSelectedParam(content);
bool surroundSelectedText = (indexOfSelectedParam >= 0);
TextSelection selected = _applicationObject.ActiveDocument.Selection as TextSelection;
EditPoint topPoint = selected.TopPoint.CreateEditPoint();
EditPoint bottomPoint = selected.BottomPoint.CreateEditPoint();
if (surroundSelectedText)
{
string beforeSelectedParam =
CodeTemplateManager.Instance.GetTextBeforeSelectedParam(content);
string afterSelectedParam =
CodeTemplateManager.Instance.GetTextAfterSelectedParam(content);
topPoint.LineUp(1);
topPoint.EndOfLine();
topPoint.Insert(Environment.NewLine);
topPoint.Insert(beforeSelectedParam);
bottomPoint.EndOfLine();
bottomPoint.Insert(Environment.NewLine);
bottomPoint.Insert(afterSelectedParam);
}
else
{
topPoint.Delete(bottomPoint);
topPoint.Insert(content);
}
}
这里的做法,不是将模板内容生成后整体替换选定文本,而是将模板内容分为三部分:选定文本之前的文本、选定文本本身和选定文本之后的文本,这样我们只要在选定文本的前面和后面分别写入文本即可。如果模板不包含 ,这里就认为将使用模板内容替换选定文本,其做法是先删除选定文本,在写入模板内容,这样处理后,速度快了很多。
现在这个功能在我所在的小团队已经开始使用了,已经节省了不少时间:)
可以从这里下载代码,也可以在这里下载可运行的Add-In(解压缩后将文件放在[My Documents Path]\Visual Studio 2008\Addins下)。
我们身在何处?
本文主要介绍了对文本编辑器的操作,相关的接口及其成员数量众多,这给了我们很大的空间来扩展VS的编辑器。本文的例子只是使用了其中的少数几个方法就完成了一个比较实用的功能,接下来我还会介绍与编辑器相关的另一个扩展PropertyManager,对代码中的属性做简单的处理。如果你有这方面的需求,望不吝分享:)
参考
《Professional Visual Studio® 2008 Extensibility》
《Working with Microsoft Visual Studio® 2005》