7 分钟阅读

最近VS2010 Extension在Visual Studio Blog(http://blogs.msdn.com/visualstudio/) 上提得很频繁,于是也想翻来文档研究研究,结果居然找了半天, 居然没有一丁点完整介绍这一块的,于是,只好自己找着VS IDE上的模板提供的内容和Visual Studio Blog上的讲解,一边Reflector参演,一边涂鸦一些代码,准备实弹演练一下, 但是觉得这个模板建出来的Extension也太简单了,刚好看到AxTool(http://www.axtools.com/products-vs2010-extensions.php) 有一个代码编辑器扩展, 也是VS Extension的,于是就照着这个,自己一步一步做一下。

基础篇

首先,要想建立VS Extension工程,你需要安装VS2010 SDK,目前是Beta2版本,你可以到这里可以下载:http://go.microsoft.com/fwlink/?LinkID=165597), 这里我是通过Editor Text Adornment模板创建的工程,嗯,我就不详细写如何通过模板创建自己Extension工程了, 如果你不熟悉这里,可以参考Quan To的这篇帖子——Building and publishing an extension for Visual Studio 2010

建好工程以后,会自动生成 TextViewCreationListener ,这里实现了 IWpfTextViewCreationListener 接口,并通过MEF导出 IWpfTextViewCreationListener 对象:

1
2
3
4
5
6
7
8
9
10
    [TextViewRole("DOCUMENT")]
    [Export(typeof(IWpfTextViewCreationListener))]
    [ContentType("text")]
    internal sealed class PETextViewCreationListener : IWpfTextViewCreationListener
    {
        void IWpfTextViewCreationListener.TextViewCreated(IWpfTextView textView)
        {
            //...
        }
    }

这样VS就会在合适的时候调用 IWpfTextViewCreationListener.TextViewCreated 方法来通知文字编辑的状态改变。

为了实现浮动一个自己的工具栏,这里还需要导出一个 AdornmentLayerDefinition , 并通过 Order Attribute 来定制这个Adornment层的显示位置和次序:

1
2
3
4
5
6
7
8
        [Name("QuickToolbarAdornmentLayer")]
        [Order(After = "Text")]
        [Export(typeof(AdornmentLayerDefinition))]
        public AdornmentLayerDefinition QuickToolbarLayerDefinition
        {
            get;
            set;
        }

这里的 Name Attribute 很重要,以后的代码中要获取我们的 AdornmentLayer 就得靠它了:

1
adornmentLayer = this._textView.GetAdornmentLayer("QuickToolbarAdornmentLayer");

扯得远了,回到 IWpfTextViewCreationListener.TextViewCreated ,通过这里,可以得到一个 IWpfTextView

这是所有操作的目标和展现,另外,还需要挂他的 ClosedLayoutChangedMouseHoveredSelectionChanged 等事件,以响应用户行为。

由于我们要通过工具栏操作代码,所以需要通过MEF导入 IEditorOperationsFactoryService

1
2
3
4
5
6
        [Import]
        internal IEditorOperationsFactoryService EditorOperationsFactoryService
        {
            get;
            set;
        }

这样就可以在 IWpfTextViewCreationListener.TextViewCreated 中通过 IEditorOperationsFactoryService.GetEditorOperations(ITextView) 来获得 IEditorOperations ,有了它,就可以方便快捷的编辑代码了。

接下来要实现工具栏的界面,这个就不多说了,建一个 UserControl ,里面放上 ToolBar 就搞定了。 那么何时何地显示这个 ToolBar 呢?这就要依赖 IWpfTextViewSelectionChanged 事件了,上面提到会挂这个事件就是为这里用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
                 private void MayBeAdornmentShowCondition()
                 {
                     if (!this._textView.Selection.IsEmpty)
                     {
                         SnapshotPoint startPos = this._textView.Selection.Start.Position;
                         SnapshotPoint endPos = this._textView.Selection.End.Position;
                         IWpfTextViewLine textViewLineContainingBufferPosition = this._textView.GetTextViewLineContainingBufferPosition(startPos);
                         TextBounds characterBounds = textViewLineContainingBufferPosition.GetCharacterBounds(startPos);
                         TextBounds bounds2 = this._textView.GetTextViewLineContainingBufferPosition(endPos).GetCharacterBounds(endPos);
                         if (this._fromMouseHover)
                         {
                             this._mustHaveAdornmentDisplayed = true;
                         }
                         else
                         {
                             PELeftButtonMouseProcessor property = null;
                             try
                             {
                                 property = this._textView.Properties.GetProperty<PELeftButtonMouseProcessor>(typeof(PELeftButtonMouseProcessor));
                             }
                             catch
                             {
                             }
                             this._mustHaveAdornmentDisplayed = (property != null)
                                 && (property.IsLeftButtonDown
                                 || ((DateTime.Now - property.LastLeftButtonDownTime).TotalMilliseconds < 400.0));
                         }
                         if (this._mustHaveAdornmentDisplayed)
                         {
                             TextBounds selectionBounds = !this._textView.Selection.IsReversed ? bounds2 : characterBounds;
                             int offset = 7;
                             double top = selectionBounds.Top + (!this._textView.Selection.IsReversed ? (offset + textViewLineContainingBufferPosition.Height) : (-offset - this._adornmentUI.ActualHeight));
                             if (top < 0.0)
                             {
                                 top = 0.0;
                             }
                             double left = characterBounds.Left + ((bounds2.Left - characterBounds.Left) / 2.0);
                             if ((left + this._adornmentUI.ActualWidth) > this._textView.ViewportWidth)
                             {
                                 left = this._textView.ViewportWidth - this._adornmentUI.ActualWidth;
                             }
                             Canvas.SetTop(this._adornmentUI, top);
                             Canvas.SetLeft(this._adornmentUI, left);
                             long chars = 0L;
                             try
                             {
                                 chars = this._textView.Selection.SelectedSpans[0].Span.Length;
                             }
                             catch
                             {
                             }
                             this._adornmentUI.SetStatus(chars);
                             this.RenderSelectionPopup();
                         }
                     }
                     else
                     {
                         this._mustHaveAdornmentDisplayed = false;
                         this._adornmentLayer.RemoveAdornmentsByTag(this._adornmentTag);
                     }
                 }
 
                 private void RenderSelectionPopup()
                 {
                     if (this._mustHaveAdornmentDisplayed)
                     {
                         IAdornmentLayerElement element = null;
                         try
                         {
                             element = this._adornmentLayer.Elements.First<IAdornmentLayerElement>(
                                 (IAdornmentLayerElement ile) => ile.Tag.ToString() == this._adornmentTag);
                         }
                         catch (InvalidOperationException)
                         {
                         }
                         if (element == null)
                         {
                             this._adornmentLayer.AddAdornment(this._textView.Selection.SelectedSpans[0], this._adornmentTag, this._adornmentUI);
                         }
                         this._timer.Stop();
                         this._timer.Start();
                     }
                 }
 
                 private void selection_SelectionChanged(object sender, EventArgs e)
                 {
                     this._fromMouseHover = false;
                     this.MayBeAdornmentShowCondition();
                 }

然后要注意的是IWpfTextViewClosed 事件处理要记得取消所有挂这个事件等等收尾工作。

接下来编译工程,打包VSIX就完成了,目前实现的主要Feature:

  1. 当在代码编辑器中选择一段文字,并将鼠标移到文字区域时,QuickToolbar会以半透明的方式“浮”文字的旁边。
  2. 当鼠标移到QuickToolbar区域,QuickToolbar会变成不透明,其上的按钮会响应鼠标动作。
  3. 目前支持的操作有:
    • 剪切(Cut)
    • 复制(Copy)
    • 粘贴(Paste)
    • 删除(Delete)
    • 减小缩进(Decrease Indent)
    • 增加缩进(Increase Indent)
    • 注释代码(Comment)
    • 取消注释(Uncomment)

调用VS内部命令

在基础篇里,主要展示了如何使用MEF扩展VS2010,来扩展编辑控制和展现自己的UI; 在实现QuickToolbar的时候,发现MEF仅仅提供了很基本的编辑控制,如果需要高级的操作,比如注释选择的代码,就捉襟见肘,很是麻烦。

本篇我将展示如何深入挖掘VS2010 Extension,使它成为锋利的军刀,而不是绣花枕头。 鉴于此,这里就从上面提到了的Feature——注释和取消注释选择的代码来剖析,希望可以为大家拓宽思路,更好的利用VS2010。

首先回顾一下基础篇中的实现,当时是基于 TextViewLine 做注释代码的,这里有两个潜在问题:

  • 其一,TextViewLine,顾名思义,是“可视区域”的行,所以如果选择超出可视区域,超出的部分就没有注释掉;
  • 其二,当选择的结束位置在行的结尾时,无法实现IDE注释代码后保持Caret在选择结尾而不跳到下一行的行为,当尝试自己重新选择并移动Caret就会收到 ITextSpanshot 无效的异常。

上面提到了VS2010 Extension对编辑器的编辑行为的控制能力仅仅提供了通用的,比如Cut/Copy/Paste等等。 而其他的诸如注释/取消注释代码,添加、删除、导航到Bookmark等程序员常用功能没有暴露出来,具体可以参考接口 IEditorOperations( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.text.operations.ieditoroperations_methods%28VS.100%29.aspx ),这里列举的所有Member表达了其所支持的编辑操作。 总之,这条路只有这么几个目的地。

那么,还有其他方法吗?貌似走到了死胡同了,但是当我们使用IDE时候,却是可以很容易的通过Edit菜单找到所有的功能的,问题是,它们要怎样才能为我所用呢?

我首先想到的是在VSSDK中找找,结果一个名字看起来很顺眼的接口撞到眼里,它就是接口 IVsUIShell( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.ivsuishell%28VS.100%29.aspx ),MSDN这么说的:

This interface provides access to basic windowing functionality, including access to and creation of tool windows and document windows. provided by the environment.

也就是说这是一个由IDE提供的全局的Service,可以创建、访问工具窗口和编辑窗口。 浏览一下这个所有Member,发现了一个叫 IVsUIShell.PostExecCommand(/*...*/)( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.ivsuishell.postexeccommand%28VS.100%29.aspx )的方法, MSDN描述说通过它可以异步执行Command,那么,只要找到注释代码的Command,在通过这个接口就可以实现VS IDE一样的注释代码的Feature了。

酷毙了,就是它,当怎么得到它呢?现在请留心MSDN上的解释,就是上面我使用红色粗体表示出来的部分——这个由IDE提供的全局的Service,那么可以通过 Package.GetGlobalService(/*...*/)(http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.package.getglobalservice%28VS.100%29.aspx )来获取:

1
IVsUIShell shell = Package.GetGlobalService(typeof(IVsUIShell)) as IVsUIShell;

接下来是找到自己需要Command,然后PostExecCommand就搞定了; 而VS提供的Command有两部分组成:Guid和CommandID,这个大部分都在类 VSConstants( http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.vsconstants%28VS.100%29.aspx )里面, 以注释代码为例,其Guid是:VsConstants.VSStd2k,而 CommandIDVSConstants.VSStd2kCmdID.COMMENTBLOCK 。下面是我包装的注释和取消注释的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void ProcessComments(bool comment)
{
    IVsUIShell shell = Package.GetGlobalService(typeof(IVsUIShell)) as IVsUIShell;
    if (shell != null)
    {
        Guid std2k = VSConstants.VSStd2K;
        uint cmdId = comment ? 
                  (uint)VSConstants.VSStd2KCmdID.COMMENT_BLOCK :
                  (uint)VSConstants.VSStd2KCmdID.UNCOMMENT_BLOCK;
        object arg = null;
        shell.PostExecCommand(ref std2k, cmdId, 0, ref arg);
    }
}

至此,我们通过VSSDK提供的能力,顺利的挖掘出VS2010 Extension的部分宝藏,你是不是也有点心动,要自己去挖掘一点呢?

实现自定义配置

在上面的两篇曾提到通过VSSDK(MSDN也叫VSX)来拓宽思路,实现一些MEF Extension所不能做到的功能, 比如获取 IVsUIShell 服务来执行 Command 等等,这里我给各位看官展示如何通过VSX提供自定义配置到IDE里面。

首先创建一个Package工程,找到里面的 XX_Package.cs ,要提供自定义配置到IDE,需要在这里通过 ProviderProfileProviderOptionPage 告诉 Package 两个重要信息: 此Package 有配置信息(Profiler )以及对应该配置信息的界面,这里我从我的GotoDef extension工程里截了一张图: GotoDef extension工程

其中ProvideProfile 告诉Package 提供的Profiler 的相关信息: 关联的提供该Profiler 的类型、分类名称、页面名称、资源ID等等, VS在需要时会把保存的信息(默认在注册表里)读取并反序列化成关联的类型的对象, 在关闭Option对话框或者确认应用配置时,会把配置信息对象序列化保存(默认在注册表)。

另一个ProvideOptionPage 来指定配置信息对象和界面,它是从DialogPage 派生, 需要注意的是需要为它提供GuidClassInterface 类型; 默认情况下,显示该配置对象使用PropertyGrid ,当然,可以通过override Window属性来自定义自己的UI, 比如GoToDef中的配置UI,如下图: GoToDef中的配置UI

完成以后在VS Extension工程中引用这个Package 并添加到VSIX输出中,这样就可以使用配置了: 配置GoToDef 调用配置

至此,为VS Extension提供自定义配置的工作就完成了,具体效果如下 GoToDef中的配置