代码完成是如何工作的?
许多编辑器和IDE都有代码完成。 他们中有些人非常“聪明”,其他人不是真的。 我对更聪明的types感兴趣。 例如,我见过IDE只提供一个函数,如果它是a)在当前范围内可用b)它的返回值是有效的。 (例如,在“5 + foo [tab]”之后,它只提供返回可以添加到一个整数或variables名称的函数。)我也看到他们把更常用或最长的选项放在前面的名单。
我意识到你需要parsing代码。 但是,通常在编辑当前代码无效的时候,会出现语法错误。 当它不完整并且包含错误时,如何parsing它?
还有一个时间限制。 如果需要几秒钟的时间来完成列表,完成是无用的。 有时完成algorithm会处理数千个类。
什么是好的algorithm和数据结构呢?
我的UnrealScript语言服务产品中的IntelliSense引擎非常复杂,但是我尽可能地给出一个概述。 VS2008 SP1中的C#语言服务是我的性能目标(理由很充分)。 它还没有,但它足够快,足够准确,我可以安全地提供一个单一的字符input后的build议,而无需等待Ctrl +空格或用户input一个.
(点)。 人们从事语言服务方面的信息越多,我就应该使用他们的产品来获得更好的最终用户体验。 有很多产品,我有不幸的工作经验,没有对细节进行如此密切的关注,结果我比IDE更与IDE打架。
在我的语言服务中,它的布局如下:
- 在光标处获取expression式。 这从成员访问expression式的开始到游标结束的标识符结束。 成员访问expression式通常采用
aa.bb.cc
格式,但也可以包含方法调用,如aa.bb(3+2).cc
。 - 获取游标周围的上下文 。 这是非常棘手的,因为它不总是遵循相同的规则作为编译器(长篇小说),但在这里假设它的确如此。 通常这意味着获得关于游标所在的方法/类的caching信息。
- 假设上下文对象实现了
IDeclarationProvider
,在这里你可以调用GetDeclarations()
来得到在范围中可见的所有项的IEnumerable<IDeclaration>
。 在我的情况下,这个列表包含本地/参数(如果在方法中),成员(字段和方法,只有静态,除非在实例方法中,并且没有基types的私有成员),全局variables(语言I的types和常量正在研究)和关键字。 在这个列表中将是一个名称为aa
的项目。 作为评估#1中expression式的第一步,我们从上下文枚举中select名称为aa
,为下一步提供一个IDeclaration
。 - 接下来,我将运算符
IDeclaration
表示aa
的IDeclaration
以获得另一个包含aa
的“成员”(某种意义上)的IEnumerable<IDeclaration>
。 自从.
运算符与->
运算符不同,我调用declaration.GetMembers(".")
并期望IDeclaration
对象正确应用列出的运算符。 - 这一直持续下去,直到我打到
cc
,声明列表中可能包含或不包含名称为cc
的对象。 我相信你知道,如果多个项目以cc
开头,它们也应该出现。 我通过最后的枚举来解决这个问题,并通过我logging的algorithm传递给用户,为用户提供最有用的信息。
以下是IntelliSense后端的一些附加说明:
- 我广泛使用LINQ的惰性评估机制来实现
GetMembers
。 我的caching中的每个对象都能够提供一个评估其成员的函数,因此使用该树执行复杂的操作几乎是微不足道的。 - 而不是每个对象保持其成员的
List<IDeclaration>
,我保留一个List<Name>
,其中Name
是一个包含描述该成员的特殊格式string的散列的结构。 有一个巨大的caching,将名称映射到对象。 这样,当我重新parsing文件时,我可以从caching中删除文件中声明的所有项目,并使用更新后的成员重新填充。 由于仿函数的configuration方式,所有expression式立即评估为新项目。
智能感知“前端”
当用户键入时,文件在语法上的错误次数往往比正确的次数多。 因此,我不想在用户input时随意删除caching的各个部分。 我有大量的特殊规则来尽快处理增量更新。 增量caching只保存在一个打开的文件的本地,有助于确保用户不会意识到他们的键入会导致后端caching为文件中的每个方法等内容保留不正确的行/列信息。
- 一个救赎因素是我的parsing器速度很快 。 它可以在低优先级后台线程上独立运行时,在150ms内处理20000行源文件的完全caching更新。 只要这个parsing器成功地完成了一个打开文件的传递(语法上),文件的当前状态就会被移入全局caching。
- 如果文件在语法上不正确,我使用ANTLRfilterparsing器(对于链接抱歉 – 大多数信息位于邮件列表中,或者通过阅读源文件收集)来重新分析文件:
- variables/字段声明。
- 类/结构定义的签名。
- 方法定义的签名。
- 在本地caching中,类/结构/方法定义从签名处开始,并在大括号嵌套级别返回到偶数时结束。 如果达到另一个方法声明(没有嵌套方法),方法也可以结束。
- 在本地caching中,variables/字段链接到前一个未closures的元素。 请参阅下面的简短代码片段,了解为什么这很重要。
- 此外,随着用户键入,我保持一个重新映射表标记添加/删除的字符范围。 这用于:
- 确保我可以识别光标的正确上下文,因为方法可以在完全parsing之间的文件中移动。
- 确保转到声明/定义/引用在打开的文件中正确定位项目。
上一节的代码片段:
class A { int x; // linked to A void foo() // linked to A { int local; // linked to foo() // foo() ends here because bar() is starting void bar() // linked to A { int local2; // linked to bar() } int y; // linked again to A
我想我会添加我用这个布局实现的智能感知function列表。 每个图片都位于这里。
- 自动完成
- 工具提示
- 方法提示
- class级视图
- 代码定义窗口
- 呼叫浏览器(VS 2010终于把这个添加到C#)
- 语义正确查找所有引用
我不能确切地说明某个特定实现所使用的algorithm,但我可以做出一些有教育意义的猜测。 对于这个问题, trie是一个非常有用的数据结构:IDE可以在项目中的所有符号的内存中维护一个较大的trie,每个节点都有一些额外的元数据。
当你input一个字符的时候,它沿着树状结构中的一条path走下去。 所有的特里特里节点的后代都是可能的完成。 IDE只需要在当前上下文中筛选出那些有意义的内容,但是只需要计算与Tab-completionpopup窗口中显示的数量一样多的内容即可。
更高级的制表符完成需要更复杂的制度。 例如, Visual Assist X有一个function,只需要键入CamelCase符号的大写字母 – 例如,如果您键入SFN,则会在其制表符完成窗口中显示符号SomeFunctionName
。
计算trie(或其他数据结构)确实需要parsing所有的代码才能得到项目中所有符号的列表。 Visual Studio将其存储在其智能感知数据库中,一个存储在项目旁边的.ncb
文件,以便在每次closures并重新打开项目时都不必重新parsing所有内容。 当你第一次打开一个大型项目(比如刚才同步源代码控制的项目)时,VS会花时间parsing所有东西并生成数据库。
我不知道它是如何处理增量变化的。 正如你所说的,当你编写代码的时候,90%的时间是无效的,每当你闲置的时候重写所有的东西都会对你的CPU造成巨大的损失,特别是如果你正在修改头文件大量的源文件。
我怀疑它或者(a)只在你实际build立你的项目(或者你可能closures/打开它)时才会进行parsing,或者(b)它会进行某种本地parsing,只parsing你刚才的代码编辑一些有限的方式,只是为了得到有关符号的名称。 由于C ++有这样一个非常复杂的语法,如果你使用大量的模板元编程等,它可能会在黑暗的angular落performance奇怪。
以下链接将进一步帮助您
语法高亮显示: 用于语法高亮显示的快速彩色文本框