操作文本编辑器
编程时始终优先使用键盘而非鼠标”但问题在于VS面向的是所有的开发人员群体它只能够提供最通用的功能如果对VS的编辑器有些额外的需求我们只好自己动手了本文将介绍如何扩展文本编辑器。AOM中编辑器相关的接口跟以前一样这里首先简单介绍一下AOM中的相关接口、类型。1Documents在默认情况下VS会以标签式文档呈现打开的各个文档。这些文档的集合在AOM中就是Documents它实现了IEnumerable接口。通过该接口我们可以获取当前打开的文档它的重要属性和方法有Count打开文档的数目Add()向集合中添加新的文档CloseAll()关闭所有文档它的参数为vsSaveChanges枚举可提供关闭时的行为选项比如提示用户进行保存Item()根据索引获取集合中的某个文档Open()打开一个文档SaveAll()保存所有文档。这些成员的含义是相当简单、直白的。我们可以通过循环变量所有打开的文档以获取所有文档的信息对于单个文档来说它对应于Document接口。2Document表示在VS中打开进行编辑的文档。它的成员较多这里仅介绍一下比较重要的几个FullName/Path/Name文档的全名、所在目录、文件名Language文档的语言类型如CSharpProjectItem获取与文档关联的ProjectItem对象Selection文档中的选定内容Type文档的类型Activate()将焦点移至该文档Close()关闭文档Redo()/Undo()执行Redo/Undo操作Save()保存文档。关于Document成员的详细信息请参看这里。其中的Selection属性非常有用因为很多时候我们都是先选中文档的部分内容再进行相应的操作。另外在打开的多个文档中只有一个处于活动状态可以使用DTE.ActiveDocument属性来快速获取该文档。在获取文档的引用后下一步就可以考虑如何进行编辑了。我们得了解5个接口TextSelection、TextPoint、EditPoint、VirtualPoint、TextDocument。相信在了解了这些接口后你在操作编辑器时会得心应手的。3TextSelection该接口提供对文档的编辑操作和选定文本的访问。它的成员比Document还有多很多功能非常全面应当可以满足绝大部分需要了这里就不再一一列举了可以参看MSDN的内容。我们在手工输入代码时可以看作总是在光标处输入也可以把光标看作一个点这个点包含一些信息如行号、列号等这样VS就可以处理输入的内容在Add-In中以编程方式输入时与此类似这个“点”就是TextPoint。4TextPoint该接口表示文档中的某个位置EditPoint和VirtualPoint继承于此。它的主要属性和方法有AbsoluteCharOffset从文档开始计算的绝对字符位置从1开始AtEndOfDocument/AtEndOfLine指示该点是否处于文档/行的结尾AtStartOfDocument/AtStartOfLine指示该点是否处于文档/行的开头DisplayColumn显示列号Line行号LineCharOffset该点在行内的位置LineLength该点所在行的字符数CreateEditPoint()创建一个EditPoint对象以对文档进行编辑EqualTo()/GreaterThan()/LessThan()与另一个TextPoint比较相互的位置关系关于TextPoint的所有成员信息请参看这里。光有TextPoint还不能编辑要真正进行编辑得使用EditPoint接口。5EditPointEditPoint从TextPoint那里继承了所有的属性和方法它还提供了很多用于编辑的属性和方法比如常见的插入、删除、剪切、粘帖、书签操作还有位置的移动等等使我们在编辑文本时拥有了强大的能力。关于EditPoint的所有成员信息请参看这里。有时一行内的字符数很多此时在屏幕能就看不到了也就是说超出了文档的右边距要操作在右边距之外的文本需要VirtualPoint。6VirtualPointVirtualPoint也继承自TextPoint只是添加了少数几个属性和方法这里就不再赘述了可以参看这里。7TextDocument最后一个接口是TextDocument它表示在编辑器中打开的文档。在你了解了前面几个接口的成员后对TextDocument的成员也很容易了解了。在操作文本时大部分时候可以选择从TextSelection开始不过在某些情况下TextDocument是个不错的开始可以考虑先使用TextDocument如果不能满足需要再转向前面的几个接口。在介绍了这么多接口之后该看一个例子了。CodeTemplate示例0问题分析这一次要给NEnhancer添加的功能是代码模板。它源自我当前的项目需要项目要求每次修改代码都要添加这样的注释C# Code - 代码中的一种注释//-------------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中都不是问题。可以建立这样的模板C# Code - 注释的简单模板//-------------Change Begin------------------------------------//-----------Code change log for item -------------------------------//-----------Modified by : authorℎ Change Date: today//-----------Changes Begin------------------------------------------------------selected//-----------Changes End for item ----------------------------------我们把一些变化的文本提取出来即item、authorℎ作为参数进行配置供当前这个模板使用而对于日期可将today作为“内置”或“全局”的参数每个模板都可以使用。在使用Add-In插入代码模板时只要将各个参数的信息替换到模板中然后插入到文档中即可。根据这样的思路可以建立如下的模板文件XML Code - 代码模板的配置文件在节点builtInParams下可以定义多个“全局”参数应用于多个模板对于日期类型的参数来说可以指定格式每个template节点定义了一个模板模板可以有自己的参数selected参数比较特别其作用是指示选中文件所放的位置最后约定所有的参数都放在两个“$”中间。下面来看看如何实现上面的思路。1添加命令在添加命令前看看NEnhancer当前的代码里面有很多代码是比较通用的每次开发Add-In都可以使用所以把它提取出来放到DTEHelper类中。在以前添加命令时往往使用这样的代码C# Code - 添加命令的第一种方式// Add commandCommand 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方法中添加代码来实现该命令这样可以每次添加一个菜单项。这对于当前的例子就不太合适了因为模板可以是多个即使根据配置文件动态添加这些菜单项如果模板多了菜单就会变得很长。一个解决方案是添加嵌套菜单将所有这些模板对应的菜单放在一个子菜单下。这里介绍添加菜单的另一种方式C# Code - 添加弹出菜单和菜单项// 向命令栏添加一个弹出菜单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运行时读取配置文件根据模板的个数生成相应个数的菜单项。界面看起来差不多是这样的菜单项的标题是配置文件中模板的名字并且所有这些菜单项使用同一个处理函数C# Code - 命令的事件处理函数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也就是前面提到的selected参数事实上到这里除此参数之外的参数都已置换完毕所以可将当前选中的文本放入作为代码模板的最终内容。这样做是可以将代码插入了不过速度有些慢我也没找到原因不过倒可以借此机会换一种方式来演示其它API的使用C# Code - 使用EditPoint编辑文档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);}}这里的做法不是将模板内容生成后整体替换选定文本而是将模板内容分为三部分选定文本之前的文本、选定文本本身和选定文本之后的文本这样我们只要在选定文本的前面和后面分别写入文本即可。如果模板不包含selected这里就认为将使用模板内容替换选定文本其做法是先删除选定文本在写入模板内容这样处理后速度快了很多。