• VS Extension+NVelocity系列(三)——让VS支持 NVelocity的智能提示(中)


    一、定义

    我们知道,我们的插件是服务于NVelocity的,在你的项目当中,对于NVelocity的模板应当有一个统一的文件扩展名,以便于VS在打开指定扩展名的文件后,就能起到具体的作用。

    如果我没有记错,Castle Monorail MVC 的NVelocity模板一律为.vm文件,本例也以.vm为准。

    在项目上新建一个NVDefinition类,内容如下

    internal static class NVDefinition
    {
        public const string ContentType = "vm";
    
        public const string FileExtension = ".vm";
    
        [Export]
        [Name("vm")]
        [BaseDefinition("HTML")]
        internal static ContentTypeDefinition nvContentTypeDefinition = null;
    
        [Export]
        [FileExtension(FileExtension)]
        [ContentType("vm")]
        internal static FileExtensionToContentTypeDefinition nvFileExtensionDefinition = null;
    }

    其中ContentType和FileExtension是为方便我们在以后的特定中使用的,比如将来要将.vm扩展名改为.rails,仅更改此处即可.

    需要注意的是BaseDefinition这个特性,VS已经预定义了以下几种类型:

    Basic、C/C++、ConsoleOutput、CSharp、CSS、ENC、FindResults、F#、HTML、JScript、XAML、XML

    简单理解就是你的内容要建立在什么内容之上,对NVelocity的代码高亮,不仅要考虑NVelocity自身的语法,还要考虑HTML、JS、CSS等,我们可以通过工具->选项->文本编辑器->文件扩展名->添加一个扩展名为vm并选择HTML编辑器,这会解决我们一大部分问题.

    而类型FileExtensionToContentTypeDefinition的意思是指定一个内容类型和扩展名之间的映射。

    二、IVsTextViewCreationListener

    回想一下我们平时写代码时的情景,当你键入一个<符号时,VS就会弹出html的标签列表或者即将匹配的尾标签,当你在js的对象上键入.符号时,弹出的将是该对象的方法和属性

    因为NVelocity的变量符号均为$,关键字为#,目前我们更关心$符号,因为NVelocity的关键字没几个,能着色就够了.我们希望能键入$符号后,弹出来一组我们自定义的Helper方法,像这样:

    1

    如何捕捉到$符号,就要依靠下面这个接口了,在解决方案上新建一个目录,命名为Intellisense,在此文件夹下新建一个CompletionController类,删掉生成的CompletionController类代码和该类命名空间的.Intellisense部分

    引用以下组件

    Microsoft.VisualStudio.Editor

    Microsoft.VisualStudio.Language.Intellisense

    Microsoft.VisualStudio.TextManager.Interop(7.1.40304.0)

    添加以下类内容:

    [Export(typeof(IVsTextViewCreationListener))]
    [ContentType(NVExtension.ContentType)]
    [TextViewRole(PredefinedTextViewRoles.Interactive)]
    [FileExtension(NVExtension.FileExtension)]
    internal sealed class VsTextViewCreationListener : IVsTextViewCreationListener
    {
        [Import]
        IVsEditorAdaptersFactoryService AdaptersFactory = null;
    
        [Import]
        ICompletionBroker CompletionBroker = null;
    
        public void VsTextViewCreated(IVsTextView textViewAdapter)
        {
            IWpfTextView textView = AdaptersFactory.GetWpfTextView(textViewAdapter);
            IOleCommandTarget next = null;
            VMCommandFilter filter = new VMCommandFilter(textView, CompletionBroker, next);
            textViewAdapter.AddCommandFilter(filter, out next);
        }
    }
    IVsTextViewCreationListener便是我们需要的这样一个监听器接口,每当一个文件被打开时,VS会检查标记有[Export(typeof(IVsTextViewCreationListener))]特性的插件,一旦确认他所标记的ContentType和FileExtension后,就会触发实现该接口的VsTextViewCreated事件.
    在此类上导入了编辑器适配器工厂服务以获取IWpfTextView对象,该对象将帮助我们在后文能找到光标位置等内容.
    VsTextViewCreated事件上,我们给当前的IVsTextView(有别于IWpfTextView)增加了命令过滤.也就是后文要讲的VMCommandFilter类.

    三、IOleCommandTarget接口

    按MSDN线上帮助对此接口的summary:”启用计划在对象和容器之间的命令”,如果靠这个字面意思理解,今天的文就到这里了…

    实际上,这是一个对编辑器命令进行过滤、查询,对特定命令进行操作并返回执行结果的一个接口.

    在CompletionController文件上,继续新建一个类VMCommandFilter,全部内容如下:

    internal sealed class VMCommandFilter : IOleCommandTarget
    {
        /// <summary>
        /// 当前会话
        /// </summary>
        ICompletionSession _CurrentSession;
    
        /// <summary>
        /// TextView(WPF)
        /// </summary>
        IWpfTextView _TextView { get; private set; }
    
        /// <summary>
        /// 代理
        /// </summary>
        ICompletionBroker _Broker { get; private set; }
    
        /// <summary>
        /// 执行由VMCommandFilter未执行完的命令
        /// </summary>
        IOleCommandTarget _Next { get; set; }
    
        public VMCommandFilter(IWpfTextView textView, ICompletionBroker broker, IOleCommandTarget next)
        {
            this._TextView = textView;
            this._Broker = broker;
            this._Next = next;
        }
    
        /// <summary>
        /// 获取输入的字符
        /// </summary>
        /// <param name="pvaIn">输入指针</param>
        private char GetTypeChar(IntPtr pvaIn)
        {
            return (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
        }
    
        /// <summary>
        /// 执行指定的命令
        /// </summary>
        /// <param name="pguidCmdGroup">命令组的 GUID</param>
        /// <param name="nCmdID">命令 ID</param>
        /// <param name="nCmdexecopt">指定对象应如何执行命令</param>
        /// <param name="pvaIn">命令的输入参数</param>
        /// <param name="pvaOut">命令的输出参数</param>
        public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
        {
            int hresult = VSConstants.S_OK;
            char inputChar = Char.MinValue;
            if (pguidCmdGroup == VSConstants.VSStd2K)
            {
                VSConstants.VSStd2KCmdID cmd = (VSConstants.VSStd2KCmdID)nCmdID;
    
                if (cmd == VSConstants.VSStd2KCmdID.RETURN //按下回车
                    ||
                    cmd == VSConstants.VSStd2KCmdID.TAB //按下Tab
                    )
                {
                    Complete(true);
                }
                else//尚未被处理
                {
                    hresult = _Next.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); //由VS现行处理此条命令.
                    //确认hresult是否是成功处理的
                    if (ErrorHandler.Succeeded(hresult))
                    {                    
                        if (cmd == VSConstants.VSStd2KCmdID.TYPECHAR) //字符键入
                        {
    		    //获取输入值
                        inputChar = GetTypeChar(pvaIn);
                            //如果当前的会话已经被启动
                            if (_CurrentSession != null && _CurrentSession.IsStarted)
                            {
                                //执行过滤
                                _CurrentSession.SelectedCompletionSet.Filter();
                                //选择最佳匹配
                                _CurrentSession.SelectedCompletionSet.SelectBestMatch();
                                //重新计算CompletionSet
                                _CurrentSession.SelectedCompletionSet.Recalculate();
                            }
                            else if (inputChar == '$') //如果是键入了$字符
                            {
                                //获取插入符号,也就是光标位置.
                                SnapshotPoint caret = _TextView.Caret.Position.BufferPosition;
                                //文本快照
                                ITextSnapshot snapShot = caret.Snapshot;
                                //在当前插入符号位置创建积极的跟踪点
                                ITrackingPoint trackingPoint = snapShot.CreateTrackingPoint(caret, PointTrackingMode.Positive);
                                //由代理创建Completion会话
                                _CurrentSession = _Broker.CreateCompletionSession(_TextView, trackingPoint, true);
                                //启动该会话.
                                _CurrentSession.Start();
                                //添加放弃事件
                                _CurrentSession.Dismissed += (sender, args) => _CurrentSession = null;
                            }
                        }
                        else if (
                            cmd == VSConstants.VSStd2KCmdID.BACKSPACE //删除键
                            ||
                            cmd == VSConstants.VSStd2KCmdID.DELETE //删除键
                            )
                        {
                            //如果当前会话并未丢弃,执行过滤
                            if (_CurrentSession != null && !_CurrentSession.IsDismissed)
                                _CurrentSession.Filter();
                        }
                    }
                }
            }
            return hresult;
        }
    
        /// <summary>
        /// 查询该对象以获得由用户界面事件生成的一个或多个命令的状态
        /// </summary>
        /// <param name="pguidCmdGroup">命令组的 GUID</param>
        /// <param name="cCmds">命令数</param>
        /// <param name="prgCmds">数组指示命令调用方需要状态信息的 OLECMD 结构</param>
        /// <param name="pCmdText">由OLECMDTEXT返回的单个命令的状态信息</param>
        public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
        {
            return _Next.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
        }
    
        /// <summary>
        /// 确认完成或者丢弃
        /// </summary>
        private bool Complete(bool force)
        {
            if (_CurrentSession == null || !_CurrentSession.IsStarted)
                return false;
    
            //如果用户没有选择并且主动丢弃
            if (!_CurrentSession.SelectedCompletionSet.SelectionStatus.IsSelected && !force)
            {
                _CurrentSession.Dismiss();
                return false;
            }
            else
            {
                _CurrentSession.Commit();
                return true;
            }
        }
    }

    引用以下组件:

    Microsoft.VisualStudio.OLE.Interop

    Microsoft.VisualStudio.Shell.11.0

    IOleCommandTarget接口有两个方法:

    int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)

    参数pguidCmdGroup是一个命令组,可接受的值定义在Microsoft.VisualStudio.VSConstants类上,有很多,目前我们仅关注VSConstants.VSStd2K,表示Win2000的标准命令组.数nCmdID则为某个命令组下的单个命令,比如输入(VSConstants.VSStd2KCmdID.TypeChar)、回车(VSConstants.VSStd2KCmdID.Return)、Tab(VSConstants.VSStd2KCmdID.Tab)等参数pvaIn是一个输入指针,IntPtr类型不常见,不过熟悉Win32 API的童鞋应当会有映像.

    其他参数本文不用,不多解释.

    方法内容分析:

    在我们选择弹出的智能提示的操作过程中,选择回车和Tab键完成我们选择正确项目的动作.并确认或者放弃Session的会话状态(Complete)方法,这是相当必要的.

    如果并非这两个键,让VS先行对命令进行操作,确认VS已经处理完毕后,如果会话已经启动,并且处于输入状态,那么我们要针对输入进行条目的过滤,并及时选择最佳的项目,重新计算条目集合.

    如果我们捕获到了一个$符号,那么我们就要实时的创建一个Completion会话,并启动它,创建会话的过程由代理完成

    如果捕捉到正在向编辑器写入删除操作,确认当前的会话还没有被丢弃以后,就要继续执行过滤.

    int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)

    查询状态我们并不需要过多操作,由VS完成即可.

    本方法在执行完毕后,如果确认执行并处理完毕了,要返回一个VSConstants.S_OK结果.

    不得已加入一个章节(中),未完待续…

  • 相关阅读:
    VS2010-MFC(对话框:模态对话框及其弹出过程)
    VS2010-MFC(对话框:设置对话框控件的Tab顺序)
    VS2010-MFC(对话框:为控件添加消息处理函数)
    VS2010-MFC(对话框:创建对话框类和添加控件变量)
    manacher算法模板
    [洛谷P1017] 进制转换
    [洛谷P1126] 机器人搬重物
    [洛谷P1032] 字串变换
    [POI2005]SAM-Toy Cars
    [洛谷P1528] 切蛋糕
  • 原文地址:https://www.cnblogs.com/kevinchoi/p/3931016.html
Copyright © 2020-2023  润新知