zoukankan      html  css  js  c++  java
  • VSX开发之语言服务系列(8)——智能感知

    回顾

    在之前的系列中,我们除了介绍了ManagedMyC这个例子、手动构建了一个SimpleLSHost,主要的精力都放在了Lex和Yacc中。之所以这样安排,因为我觉得“内功”比“招式”重要的多。不过对于学习而言,一上来就接触深层次的东西往往是个艰苦的过程。正因为如此,在接下来的几篇中,我将要开始介绍语言服务的各种“招式”,那么这篇就先从智能感知开始吧。

    构建一个LS包

    我不打算从头开始构建一个LS包,而是从我之前创建的SimpleLSHost开始。点击下载SimpleLSHost实例。如果想要从头开始,可以参考:VSX开发之语言服务系列(4)——从空Package开始构建语言服务框架VSX开发之语言服务系列(5)——构建自己的Scanner和Pareser。我打算做这么一个简单的事情,当用户输入"$"的时候,自动提示在上下文中输入过的数字字符;当输入"@"时自动提示上下文中的小写字符串。看似是一个十分简单的需求。好吧,现在开始。

    创建触发器

    首先我们要实现"$"和"@"的触发功能,因为智能感知的窗体是“触发”出来的。至于感知的内容可以稍后讨论。

    定义标记

    因为现在"$"和"@"是两个有意义的标记,所以首先在parser.y中定义两个标记,分别表示"$"和"@"。然后重新编译工程两次。(在开发过程中,我经常发现只编译一次无法使对parser.y或lexer.lex的修改生效)

    %token DOLLAR
    %token AT

    定义触发

    在UserSupplied下的Configuration的构造函数中添加如下两行代码:

    ColorToken((int)Tokens.DOLLAR, TokenType.Keyword, Number, TokenTriggers.MemberSelect);
    ColorToken((int)Tokens.AT, TokenType.Keyword, Lowletter, TokenTriggers.MemberSelect);

    这里的Tokens.DOLLARTokens.AT便是parser .y刚定义的标记。这两句话定义了这两个标记的标记类型(无关紧要),颜色对象和触发器。这里的触发器设置成MemberSelect,因为这是智能感知的触发器。

    让扫描器返回标记

    标记定义好以后需要扫描器返回标记。在lexer.lex的第一部分中定义两个lex自己用的标记,注意这里"$"是特殊字符,需要转义。

    dollar    "\$"
    at        "@"

    添加两个匹配规则,匹配动作为返回标记,注意要在"."规则之前(即任意字符匹配之前)。


    {dollar}            { return (int)Tokens.DOLLAR;}
    {at}                { return (int)Tokens.AT;}

    添加提示信息

    定位到UserSupplied下的Resolver.cs在FindMembers方法中添加如下代码:


    List<Babel.Declaration> members = new List<Babel.Declaration>();

    members.Add(new Babel.Declaration("Tips1", "Tips1", 0, "Tips1"));
    members.Add(new Babel.Declaration("Tips2", "Tips2", 0, "Tips2"));
    return members;

    Declaration是Babel定义的对象。实际上真正的提示信息和IDE的接口是AuthoringScope中的GetDeclarations方法,该方法返回一个Microsoft.VisualStudio.Package.Declarations集合。IDE通过这个集合的接口获得用户想要显示的提示信息。Babel的Declarations继承了Microsoft.VisualStudio.Package.Declarations,并且创建了Babel.Declaration,以便我们能像上面的代码那样只是简单的向集合里面添加即可。Babel.Declaration的构造函数带有四个参数,我只解释第三个参数。第三个参数的原型是int glyph,这表示的是每条感知信息的图标,可以使用IDE内建的图标。关于这些图标的索引值,在SDK文档的The Default Image List for a Language Service主题中有描述,这里便不再重复了。但是值得注意的是,文档中给出的public enum IconImageIndex实际上是错的!在本篇的附录中,我会将这个错误纠正。

    调试

    到这里触发器定义好了,我们可以看看效果,重新编译工程,并执行。(如果读者是下载SimpleLSHost的。可能会因为安装VS路径与我不同而无法启动。这时请检查工程属性中Debug所使用的外部程序路径。)打开一个.sls文件。下面是效果图:

    image image

    可以看到当输入"$"或"@"时都会出现智能提示,这个提示使用了"public class"式样的图标,并显示了Tips1和Tips2。当然这么简单的静态的提示,对于实际的语言服务是没有太大意义的,接下来,我们做些有意义的事情。

    了解智能提示的工作过程

    在接下去之前,我打算把上面部分的工作过程梳理一下,以便读者能更容易理解接下来的工作。在前面介绍着色器工作原理的章节中,我提到过IScanner接口。这是Scanner和IDE表现层的接口,其中ScanTokenAndProvideInfoAboutIt方法会被IDE反复调用并返回TokenInfo结构,TokenInfo包含标记的着色信息,类型信息,触发器信息等。IDE在获得一个标记之后决定下一步的处理,而Babel实现了一个LineScanner,并实例化Scanner:

    Babel.ParserGenerator.IColorScan lex = null;

    this.lex = new Babel.Lexer.Scanner();

    于是在ScanTokenAndProvideInfoAboutIt中,LineScanner根据Configuration中的“配置”,给TokenInfo赋值。因此,Configuration实际上便成了“配置”文件,可以在Configuration中定义触发器。

    在TokenInfo返回给IDE后,如果其中是个MemberSelect触发器,IDE会调用LanguageService类中的ParseSource方法,这个方法返回一个AuthoringScope,IDE再调用AuthoringScopeGetDeclarations方法,得到感知的信息列表。ParseSource还会传入ParseRequest对象,这个对象包含ParseReason枚举,可以在ParseSource方法中判断ParseReason是不是MemberSelectAndHighlightBraces,如果是可以为返回的AuthoringScope对象做特殊处理。Babel还为用户设计了IASTResolver接口,并实现了一个继承的AuthoringScope,在这个AuthoringScope中调用IASTResolver接口的FindMembers方法。我们需要做的便是在Resolver中实现这个方法,上面我们就是这样做的。

    下图简要说明了这个过程:

    image

    使用Parser获得上下文信息

    LanguageService.ParseSource方法

    理解了上面这个过程,应该想到的是如果想要把上下文的信息加入到智能提示信息中,就像这个实例的需求那样,关键点在于ParseSource方法返回的这个AuthoringScope能不能为我们提供足够的上下文信息。好在,ParseSource方法实例化了一个解析器,并调用了Parse方法进行了解析,看下面四行代码:

    Babel.Lexer.Scanner scanner = new Babel.Lexer.Scanner();

    Parser.Parser parser = new Parser.Parser();

    parser.scanner = scanner;

    yyparseResult = parser.Parse();

    可以看出parser使用的扫描器就是Babel.Lexer.Scanner,跟IDE表现层使用的是同一个Scanner!!下图总结了Scanner与其实现的接口的关系:

    image

    Parse方法是唯一能处理底层解析,并使用我们定义的语法区分标记的方法。但是这个方法只返回成功与否,光从返回值不能获得足够的信息。所以,只能在Parser中定义一些字段,并在解析的过程中将信息保存在Parser中

    部分类Parser

    现在我们定位到ManagedBabel下的Parser.cs中,这个Parser是个部分类,并且存在于Babel.Parser名字空间下,另一个Parser便是parser.y生成的Parser。于是,我们便可以在这里的Parser中定义我们自己的字段。观察现有的Parser可以发现其中已经定义了一些额外的字段和方法,其中包括一个IList<TextSpan[]> braces;这是原先的ManagedMyC为了实现括号匹配用的,在以后的文章中,将介绍如何实现括号匹配。

    底层解析与扫描处理

    OK,现在开始实现我们的业务吧。为了保存上下文中含有的数字和小写字母,我们必须定义两个存放它们的List,在Parser中定义两个字段,我们暂时使用public修饰,并在初始化函数中初始化它们:

    public IList<TextSpan> numbers;
    public IList<TextSpan> literals;

    public void MBWInit(ParseRequest request)
    {

       ...
       numbers = new List<TextSpan>();
       literals = new List<TextSpan>();
    }

    在Parser中临时定义如下三个辅助方法:

    //add numbers
    private void AddNumbers(params Babel.ParserGenerator.LexLocation[] locs)
    {
         foreach (Babel.ParserGenerator.LexLocation l in locs)
         {
             numbers.Add(LocationToSpan(l));
         }
    }

    //add literals
    private void AddLiterals(params Babel.ParserGenerator.LexLocation[] locs)
    {
         foreach (Babel.ParserGenerator.LexLocation l in locs)
         {
             literals.Add(LocationToSpan(l));
         }
    }

    //convert from location to textspan
    private TextSpan LocationToSpan(Babel.ParserGenerator.LexLocation s)
    {
         TextSpan ts = new TextSpan();
         ts.iStartLine = s.sLin - 1;
         ts.iStartIndex = s.sCol;
         ts.iEndLine = s.eLin - 1;
         ts.iEndIndex = s.eCol;
         return ts;
    }

    在parser.y中修改语法定义部分的匹配行为代码,如下。这里的含义是,当序列NUMBERLOWLETTER 规约时执行上面定义了两个辅助函数。

    Declaration
        : NUMBER            { AddNumbers(@1);}
        | CAPLETTER       
        | LOWLETTER            { AddLiterals(@1);}
        ;

    这里"@1"会被MPPG编译成location_stack.array[location_stack.top-1],这是个LexLocation;同理,"@2"会被编译成location_stack.array[location_stack.top-2]。在之前文章中我提到过LexLocation表示标记的位置信息,这里"@1"表示NUMBER标记的位置,我们将这些位置信息在解析过程中保存在Parser对象中。

    为了使位置信息经由扫描器传递给解析器,需要在lexer.lex的第一部分加入如下代码,这段代码来自于ManagedMyC:

    %{
           internal void LoadYylval()
           {
               yylval.str = tokTxt;
               yylloc = new LexLocation(tokLin, tokCol, tokLin, tokECol);
           }
    %}

    在lexer.lex的第二部分加入如下代码:

    %{
                          LoadYylval();
    %}

    这样在每个标记扫描后,会把每个标记的yylloc、yylval加载,这样解析器就可以通过yylloc、yylval访问标记的位置信息和值信息。

    表层处理

    完成上述过程后,ParseSource方法便可以获得我们想要的上下文信息了,只不过这个信息还只是标记的位置信息,我们需要的是文本信息,可以利用ParseSource中的Source对象将位置信息转化成文本。可能有些读者会问:为什么要将位置信息保存下来,不直接保存文本吗?事实上,的确如此,在这里我只是为了演示SourceGetText方法。在实际的使用中,建议直接利用标记的值信息,这涉及到"$n"的用法将在以后的文章中涉及。还有一个问题,提供智能感知信息列表的是Resolver,Resolver是无法直接访问到Parser的,于是,我们不得不将转化的结果保存在Resolver对象中。

    在Resolver中定义两个共有字段,用来存储:

    public List<string> numbers = new List<string>();
    public List<string> literals = new List<string>();

    为了方便,将ManagedMyC/AuthoringScope中的IASTResolver resolver字段改成public Resolver resolver

    在ParseSource方法开始初始化一个AuthoringScope,并在最后返回这个AuthoringScope:

    AuthoringScope asp = new AuthoringScope(null);

    ...

    return asp;

    接着在ParseSource的yyparseResult = parser.Parse();之后添加如下代码,这段代码便是将位置信息转化成文本,并存储到Resolver对象中。

    foreach (TextSpan ts in parser.numbers)
    {
        asp.resolver.numbers.Add(source.GetText(ts));
    }

    foreach (TextSpan ts in parser.literals)
    {
        asp.resolver.literals.Add(source.GetText(ts));
    }

    这样在Resolver中的FindMember方法就可以这样写了:


    List<Babel.Declaration> members = new List<Babel.Declaration>(); 
    foreach(string number in numbers)
         members.Add(new Babel.Declaration(number, number, 0, number));
    foreach (string literal in literals)
         members.Add(new Babel.Declaration(literal, literal, 0, literal));

    return members;

    编译运行,结果如下。可以看到,当我们输入"$"或者"@"的时候,智能感知了上文中小写字母和数字。image image

    还差一点。我们想要的是"$"显示数字,"@"显示小写的字符串啊。因为在Resolver对象中并没有判断标记是"$"还是"@",而是统一处理了。现在我们需要将标记信息传递给Resolver

    标记信息需要AuthoringScope传递给Resolver,所以修改AuthoringScope.GetDeclarations,修改如下这个case,把TokenInfo传给FindMembers方法。

    case ParseReason.MemberSelectAndHighlightBraces:

      declarations = resolver.FindMembers(info, line, col);

    修改FindMembers方法:

    List<Babel.Declaration> members = new List<Babel.Declaration>();
    Microsoft.VisualStudio.Package.TokenInfo token = result as Microsoft.VisualStudio.Package.TokenInfo;
    if (token != null)
    {
        switch (token.Token)
        {
            case (int)Babel.Parser.Tokens.DOLLAR:
                {
                    foreach (string number in numbers)
                        members.Add(new Babel.Declaration(number, number, 0, number));
                }
                break;
            case (int)Babel.Parser.Tokens.AT:
                {
                    foreach (string literal in literals)
                        members.Add(new Babel.Declaration(literal, literal, 0, literal));
                }
                break;
        }
    }

    return members;

    在这个方法中我们根据TokenInfo,如果是DOLLAR就显示数字,反之显示字符串。

    还有最后一步,在LineScanner的ScanTokenAndProvideInfoAboutIt方法的if (token != (int)Tokens.EOF)语句块中添加tokenInfo.Token = token;之所以要加这句话,是因为ScanTokenAndProvideInfoAboutIt返回的TokenInfo需要包含token,但是Babel却没有!!我不知道这是不是微软的失误。呵呵。如果这里TokenInfo不返回token值的话,我们就不能在表现层得知是哪个标记。

    编译运行,最后的效果是这样的。

    image image

    小结

    本篇讲解了如何在语言服务的表现层集成智能感知功能,并用一个例子说明了具体实现memberselect功能的步骤。可以在下面这个地址下载本篇的例子程序:SimpleLSHostMemberSelect

    这个例子程序是比较基本的,一个真正的语言服务要做的事情比这个多的多。读者关键要理解各部分协调的机理,这样才能举一反三。

    附录

    下面是可以直接在程序中使用的IconImageIndex。

    internal enum IconImageIndex
    {
        // access types
        AccessPublic = 0,
        AccessInternal = 1,
        AccessFriend = 2,
        AccessProtected = 3,
        AccessPrivate = 4,
        AccessShortcut = 5,

        Base = 6,
        // Each of the following icon type has 6 versions,
        //corresponding to the access types
        Class = Base * 0,
        Constant = Base * 1,
        Delegate = Base * 2,
        Enumeration = Base * 3,
        EnumMember = Base * 4,
        Event = Base * 5,
        Exception = Base * 6,
        Field = Base * 7,
        Interface = Base * 8,
        Macro = Base * 9,
        Map = Base * 10,
        MapItem = Base * 11,
        Method = Base * 12,
        OverloadedMethod = Base * 13,
        Module = Base * 14,
        Namespace = Base * 15,
        Operator = Base * 16,
        Property = Base * 17,
        Struct = Base * 18,
        Template = Base * 19,
        Typedef = Base * 20,
        Type = Base * 21,
        Union = Base * 22,
        Variable = Base * 23,
        ValueType = Base * 24,
        Intrinsic = Base * 25,
        JavaMethod = Base * 26,
        JavaField = Base * 27,
        JavaClass = Base * 28,
        JavaNamespace = Base * 29,
        JavaInterface = Base * 30,
        // Miscellaneous icons with one icon for each type.
        Error = 187,
        GreyedClass = 188,
        GreyedPrivateMethod = 189,
        GreyedProtectedMethod = 190,
        GreyedPublicMethod = 191,
        BrowseResourceFile = 192,
        Reference = 193,
        Library = 194,
        VBProject = 195,
        VBWebProject = 196,
        CSProject = 197,
        CSWebProject = 198,
        VB6Project = 199,
        CPlusProject = 200,
        Form = 201,
        OpenFolder = 202,
        ClosedFolder = 203,
        Arrow = 204,
        CSClass = 205,
        Snippet = 206,
        Keyword = 207,
        Info = 208,
        CallBrowserCall = 209,
        CallBrowserCallRecursive = 210,
        XMLEditor = 211,
        VJProject = 212,
        VJClass = 213,
        ForwardedType = 214,
        CallsTo = 215,
        CallsFrom = 216,
        Warning = 217,
    }

  • 相关阅读:
    《JS权威指南学习总结--6.6属性getter和setter》
    《JS权威指南学习总结--6.5枚举属性》
    django的模板系统
    django额外参数的传递和url命名
    python在图片上画矩形
    当爬虫遇到js加密
    php基础-7
    php基础-6
    php基础-5
    php基础-4
  • 原文地址:https://www.cnblogs.com/P_Chou/p/1747414.html
Copyright © 2011-2022 走看看