回顾
在前面一篇中,我们浏览了ManagedMyC这个例子,其中的代码虽然不算庞大,但是有很多令人困惑的地方。这一篇中,我们来再来慢慢看看代码。
Babel
其实一直很困惑,为什么微软找这么个名字。反正不管那么多,只要记住,所谓的Babel(这里特指托管的Babel)就是帮助我们开发语言服务的一系列文件。在ManagedMyC这个例子中它们被放在ManagedBabel文件夹中,而且是以连接的形式。实际上这里只是为了防止更改文件而已。现在我们先看如下几个文件:
Package.cs
Babel提供了一个继承自Microsoft.VisualStudio.Shell.Package和IOleComponent的类BabelPackage。有了这个类,我们在编写Package类的时候只需要为其提供一个GUID,其他的全部继承便可以了。的确是个方便的方式。在构造函数中声明了一个委托、并将语言服务加到当前这个服务容器中:
1 2 3 4 5 6 7 | protected BabelPackage() { ServiceCreatorCallback callback = new ServiceCreatorCallback( ... // proffer the LanguageService ( this as IServiceContainer).AddService( typeof (Babel.LanguageService), callback, true ); } |
当Package被载入的时候,它相应的服务也会载入,这里指定加载一个叫LanguageService的类,并且在加载时调用callback回调,回调函数如下:
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 | delegate (IServiceContainer container, Type serviceType) { if ( typeof (Babel.LanguageService) == serviceType) { Babel.LanguageService language = new Babel.LanguageService(); language.SetSite( this ); // register for idle time callbacks IOleComponentManager mgr = GetService( typeof (SOleComponentManager)) as IOleComponentManager; if (componentID == 0 && mgr != null ) { OLECRINFO[] crinfo = new OLECRINFO[1]; crinfo[0].cbSize = ( uint )Marshal.SizeOf( typeof (OLECRINFO)); crinfo[0].grfcrf = ( uint )_OLECRF.olecrfNeedIdleTime | ( uint )_OLECRF.olecrfNeedPeriodicIdleTime; crinfo[0].grfcadvf = ( uint )_OLECADVF.olecadvfModal | ( uint )_OLECADVF.olecadvfRedrawOff | ( uint )_OLECADVF.olecadvfWarningsOff; crinfo[0].uIdleTimeInterval = 1000; int hr = mgr.FRegisterComponent( this , crinfo, out componentID); } return language; } else { return null ; } } |
先判断载入的服务是否是LanguageService,如果是则构造一个实例,并挂载这个服务,接着注册IOleComonent对象,并返回这个服务的实例。
接着我们来讨论一下IOleComponent。大家一定想实现一些诸如错误提示的功能,大家也许也注意到,当我们在VS中以相对快的速度编写C#代码的时候,即便此时有语法错误,在代码输入的时候也不会提示,但是一旦我们停下来,这些错误便感知出来了。这是因为像错误提示这类功能是在语言服务“空闲”的时候进行的。将我们的包继承自IOleComponent并注册时,就是告诉IDE,这个包是支持空闲时的。在代码中的#region IOleComponent Members部分的代码便是实现了IOleComponent,而注册的动作是靠IOleComponentManager完成的。
另外,应当注意到的是,这里的BabelPackage有个缺点,它指定了语言服务类的名字就是Babel.LanguageService,那么就是说我们必须要有个Babel.LanguageService的类才能使这个类工作!!如果你想要自己定义一个LS的名字,或者一个更常见的情况:名字空间不一样的话,要记得到这里来改代码。
LanguageService.cs
Microsoft.VisualStudio.Package.LanguageService是个抽象类,而我们的LanguageService必须继承自这个类,并且为了实现某些功能你还必需重写其中的方法。Babel为我们构建了一个BabelLanguageService,但是没有GUID,在简单的情况下,只要继承这个BabelLanguageService并且为他提供一个GUID就可以实现我们自己的LanguageService。UserSupplied\LanguageService.cs就是这样做的:
1 2 3 4 5 6 7 8 | [Guid( "73DA124B-2CC5-4f79-A0DB-B11B6AAA2BE5" )] class LanguageService : BabelLanguageService { public override string GetFormatFilterList() { return "MyC File (*.myc)\n*.myc" ; } } |
既然BabelLanguageService这么强大,我们就来剖析它吧。
首先要花一些篇幅解释一下语法着色的工作原理,请看下图,此图是根据我的理解画的:
如图,一个LanguageService包含一个Colorizer对象,叫做着色器对象,负责对代码编辑区中的文本着色,LS(LanguageService缩写,下同)基类有个GetColorizer方法,返回默认的Colorizer实例,LS在内部调用这个方法。这个方法是可以覆盖的,但是我不建议你覆盖,因为要自己实现一个Colorizer应该不太简单,默认的应该够了。着色器对象虽然负责着色,但是它也“只会”着色,着色的原则是基于这样的规则:一个index对应一个颜色(确切的说是一个index对应一个
IVsColorableItem,IVsColorableItem可以通过IDE的Font and Color修改的,包括了前景色,背景色,加粗,划线)。也就是说,着色器需要通过调用LS中的GetColorableItem虚方法判断应该为哪个index用上什么颜色,上图的“判断着色”就是这个虚方法,我们需要在LS中重写这个方法;接下来的问题是着色器怎么知道哪些文本对应一个什么index呢?这要借助于Scanner扫描器,扫描器必须实现IScanner接口,这个接口有两个方法:ScanTokenAndProvideInfoAboutIt和SetSource,着色器会把文本通过SetSource传给扫描器,然后不断调用ScanTokenAndProvideInfoAboutIt来获取每个index。实际上扫描器返回的index不仅仅是个整型值,而是一个TokenInfo的结构,其中包含了某段文本的位置信息,类型信息,触发器信息,以及这个index(说到这里我终于打算把这个index改口为TokenColor枚举了),这段文本也有个特定的名称,称为token(标记)。OK,有点乱,我们来理顺一下思路,Colorizer将文本通过IScanner的SetSource方法传给扫描器,扫描器需要实现IScanner,接着Colorizer反复调用ScanTokenAndProvideInfoAboutIt直到扫描器返回false,通过这种多次的调用,Colorizer获得了一些标记,Colorizer对每个标记中的TokenColor调用GetColorableItem方法判断这个标记应该是什么颜色。于是整个文本着色完成。
标记的概念:由于标记的概念十分重要,我打算引用MSDN上的解释:
解析器是语言服务的核心。解析器把文本分割成语法上的标记,然后为其赋予语义上的含义
下面是一段C#代码:
namespace MyNamespace { class MyClass { public void MyFunction(int arg1) { int var1 = arg1; } } }这段文本的标记可以是:
Token Name
Token Type
namespace, class, public, void, int
keyword
=
operator
{ } ( ) ;
delimiter
MyNamespace, MyClass, MyFunction, arg1, var1
identifier
MyNamespace
namespace
MyClass
class
MyFunction
method
arg1
parameter
var1
local variable
引自:MSDN:Language Service Parser and Scanner (Managed Package Framework)
说到这里,其实已经解释完了BabelLanguageService类中#region Custom Colors部分的代码了,另外GetScanner方法就是Colorizer用来获得Scanner的方法,Babel为我们重写了这个方法,并实现了一个简单的LineScanner(在LineScanner.cs中),我们现在还不必深究LineScanner。
此外,其他重写的函数:
函数 |
说明 |
GetLanguagePreferences |
语言服务偏好 |
Name |
语言服务的名字 |
ParseSource |
每次解析文本时由LS调用,十分重要,以后会解释 |
AuthoringScope.cs
这里包含的类是AuthoringScope,是处理用户的操作的类,这些操作包括:
函数 |
说明 |
GetDataTipText |
鼠标在某个标记上的时候显示ToolTip |
GetDeclarations |
自动完成操作、成员感知 |
GetMethods |
方法参数感知 |
Goto |
Goto操作 |
如果要实现智能感知的功能,那么这个类是必须的。
其他
其他文件的内容有些比较单一,有些涉及到深层次的原理,这里只列出简介,以后的章节将涉及:
文件 |
说明 |
Configuration.cs |
部分类。顾名思义,这个类是用来配置语言服务的某些参数,比如语言服务的名字、文件后缀以及着色器所需要的信息,即一个TokenColor如何对应一个IVsColorableItem。Babel在这里为我们实现了一些通用的方法,我们在编写配置的时候只要调用这些方法即可。值得注意的是,用CreateColor方法创建的IVsColorableItem,是会出现在Font and Color中的,这是个很不错的集成特性。 |
Declaration.cs\ Declarations.cs |
通过重写Microsoft.VisualStudio.Package.Declarations,构造智能感知的感知项和感知项集合 |
IASTResolver.cs |
Babel单独定义的实现智能感知的接口,开发人员可以实现这个接口以支持智能感知 |
IScanner.cs |
注意这里与之前提到的IScanner接口不一样。扫描器其实有两个,一个给着色器用,一个给解析器用,但是核心的扫描规则只有一个,于是需要两种接口,这个文件包含了这两种接口分别是IColorScan和AScanner。详细的解释将在以后的章节中提到。 |
Method.cs\ Methods.cs |
通过重写Microsoft.VisualStudio.Package. Methods,构造函数参数感知的感知的参数等内容 |
Parser.cs |
部分类,与parser.y生成的Parser.cs构成解析器类,LS通过这个实例完成对文本的解析。这里的代码是对parser.y的扩展。(可以在工程的obj下找到由parser.y生成的parser.cs) |
ParserStack.cs\Rule.cs\ State.cs |
解析器内部需要的数据结构定义 |
ShiftReduceParser.cs |
解析器需要的“移进”和“规约”行为的详细实现,Babel在这个类中为我们完成的是行为代码,我们在parser.y需要的是语法定义和配置。 |
Source.cs |
继承自Microsoft.VisualStudio.Package.Source,是对Source对象的扩展。Babel在这里扩展了括号匹配和注释符号。 |
UserSupplied
UserSupplied文件夹下的文件大多是开发人员可以对Babel的定制扩展,下面以表格的形式列出:
文件 |
说明 |
Configuration.cs |
部分类。与Babel的Configuration共同构成配置,详见上文 |
LanguageService.cs |
语言服务类,简单继承BabelLanguageService,提供一个GUID,注意这里的类名必须是LanguageService,名字空间必须是Babel,如果想要定制,需要修改Babel中的部分代码。 |
Package.cs |
包对象 |
Resolver.cs |
用户实现IASTResolver,以支持智能感知。 |
Generated
Generated文件夹下的文件,值得深究的将是lexer.lex和parser.y,他们分别是扫描器和解析器的核心“配置文件”,分别是以lex风格和Yacc风格的代码形式呈现的,在以后的章节中将专门讨论。另外,ErrorHandler.cs和LexDefs.cs是用来搜集目标代码中的语法错误的。
小结
在本篇中,我们详细看了示例程序的所有代码文件,并且我对其中的某些代码做了详细的分析。看完本篇后,应当对语言服务有了更深的理解,尤其应该理解代码着色的实现原理,我建议读者再深入研究一下Configuration.cs,这样可以完全理解代码着色。另外标记的概念尤为重要,在以后的篇幅中将一直提到。最后我推荐读者阅读一下MSDN中以下两个主题:Syntax Colorizing (Managed Package Framework)、Language Service Parser and Scanner (Managed Package Framework)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义