代码完成如何工作?


84

许多编辑器和IDE都有代码完成功能。其中一些是非常“智能”的,其他则不是。我对更智能的类型感兴趣。例如,我看到IDE仅在以下情况下提供功能:a)在当前范围内可用b)其返回值有效。(例如,在“ 5 + foo [tab]”之后,它仅提供返回可以添加到正确类型的整数或变量名称的函数。)我还看到它们将更常用或最长的选项放在前面列表中。

我知道您需要解析代码。但是通常在编辑当前代码时无效,其中包含语法错误。当内容不完整且包含错误时,您该如何解析?

还有一个时间限制。如果花几秒钟才能得出列表,则完成是没有用的。有时,完成算法处理数千个类。

有什么好的算法和数据结构?


1
一个好问题。您可能想看一下一些实现此功能的开源IDE的代码,例如codeblocks.org上的Code :: Blocks 。

Answers:


64

我的UnrealScript语言服务产品中的IntelliSense引擎很复杂,但是我将在此处尽可能提供概述。我的性能目标是VS2008 SP1中的C#语言服务(出于充分的理由)。它尚不存在,但是它足够快速/准确,我可以在键入单个字符后安全地提供建议,而无需等待ctrl + space或用户键入.(点)。人们(从事语言服务方面)获得的有关该主题的信息越多,如果我曾经使用他们的产品,就会获得更好的最终用户体验。我有很多不幸的工作经历,对产品没有太在意细节,因此,我与IDE的斗争比与代码的斗争还多。

在我的语言服务中,其布局如下所示:

  1. 在光标处获取表达式。从成员访问表达式的开头到光标所经过的标识符的结尾。成员访问表达式通常采用形式aa.bb.cc,但也可以包含方法调用,如中所示aa.bb(3+2).cc
  2. 获取光标周围的上下文。这非常棘手,因为它并不总是遵循与编译器相同的规则(长话短说),但是在这里假设它遵循。通常,这意味着获取有关游标所在的方法/类的缓存信息。
  3. 假设上下文对象实现IDeclarationProvider,您可以在其中调用GetDeclarations()以获取IEnumerable<IDeclaration>作用域中所有可见项的。就我而言,此列表包含局部变量/参数(如果在方法中),成员(字段和方法,仅静态(除非在实例方法中,除非是实例方法且没有基本类型的私有成员),全局变量(语言的类型和常量)正在开发)和关键字。该列表中将包含一个名称为的项目aa。作为评估#1中表达式的第一步,我们从上下文枚举中选择名称为的项目aaIDeclaration为下一步提供一个。
  4. 接下来,我将运算符应用于IDeclaration表示aa以获取另一个IEnumerable<IDeclaration>包含的“成员”(在某种意义上)aa。由于该.运算符与该->运算符不同,因此我调用declaration.GetMembers(".")并期望该IDeclaration对象正确应用列出的运算符。
  5. 这一直持续到我命中为止cc,声明列表中可能包含也可能不包含名称相同的对象cc。如您所知,如果有多个项目以开头cc,则它们也应出现。我通过采用最终枚举并将其通过我记录的算法来解决此问题,以为用户提供尽可能多的有用信息。

以下是IntelliSense后端的一些其他说明:

  • 我在实现中广泛使用LINQ的惰性评估机制GetMembers。我的缓存中的每个对象都可以提供一个对其成员求值的函子,因此对树执行复杂的操作几乎是微不足道的。
  • 而不是每个对象都保留List<IDeclaration>其成员,而是保留一个List<Name>,其中Name是一个结构,其中包含描述该成员的特殊格式字符串的哈希。有一个巨大的缓存将名称映射到对象。这样,当我重新解析文件时,可以从缓存中删除文件中声明的所有项目,并使用更新的成员重新填充它。由于函子的配置方式,所有表达式都会立即求值到新项。

IntelliSense“前端”

随着用户的输入,该文件在语法上的错误多于正确的错误。因此,我不想在用户键入时随意删除缓存的各个部分。我有大量特殊情况规则,可以尽快处理增量更新。增量缓存仅在打开的文件本地保留,有助于确保用户不会意识到自己的键入导致后端缓存为文件中的每种方法之类的东西保存了错误的行/列信息。

  • 赎回因素之一是我的解析器速度很快。它可以在150ms内处理20000行源文件的完整缓存更新,同时在低优先级后台线程上进行独立运行。每当此解析器成功完成打开文件的传递(语法上)时,文件的当前状态就会移入全局缓存。
  • 如果该文件在语法上不正确,那么我将使用ANTLR过滤器解析器(很抱歉,该链接-大多数信息位于邮件列表中或从阅读源中收集到)来重新解析该文件,以查找:
    • 变量/字段声明。
    • 类/结构定义的签名。
    • 方法定义的签名。
  • 在本地缓存中,类/结构/方法的定义从签名开始,到大括号嵌套级别回到偶数时结束。如果到达另一个方法声明(没有嵌套方法),方法也可以结束。
  • 在本地缓存中,变量/字段链接到紧接在前的未封闭元素。有关为什么这很重要的示例,请参见下面的简短代码段。
  • 另外,当用户输入内容时,我会保留一个重映射表,以标记添加/删除的字符范围。这用于:
    • 确保我可以确定游标的正确上下文,因为在完全解析之间,方法可以/确实可以在文件中移动。
    • 确保转到声明/定义/参考可以在打开的文件中正确定位项目。

上一节的代码片段:

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

我想我会添加一个使用此布局实现的IntelliSense功能的列表。每个图片都在这里。

  • 自动完成
  • 工具提示
  • 方法提示
  • 类视图
  • 代码定义窗口
  • 呼叫浏览器(VS 2010最终将其添加到C#中)
  • 语义正确查找所有参考

非常感谢。我从未想到排序时区分大小写。我特别喜欢您可以处理不匹配的括号。
stribika

15

我无法确切地说出任何特定实现使用什么算法,但是我可以做出一些有根据的猜测。一个线索是这个问题的一个非常有用的数据结构:IDE可以保持在所有项目中的符号的一个大的内存线索,在每个节点的一些额外的元数据。

当您键入一个字符时,它将沿着特里里的路径走。特定特里节点的所有后代都是可能的完成。然后,IDE仅需要根据当前上下文中有意义的过滤器将它们过滤掉,但是它只需要计算尽可能多的选项卡完成弹出窗口中显示的内容即可。

更高级的制表符完成要求更复杂的尝试。例如,Visual Assist X具有一项功能,您只需键入CamelCase符号的大写字母-例如,如果键入SFN,它将SomeFunctionName在其制表符完成窗口中显示该符号。

计算特里(或其他数据结构)确实需要解析所有代码以获取项目中所有符号的列表。Visual Studio将此.ncb文件存储在其IntelliSense数据库中,该文件与项目一起存储,因此不必每次关闭并重新打开项目时都重新解析所有内容。第一次打开大型项目(例如,您刚刚同步了表单源控件的项目)时,VS将花时间分析所有内容并生成数据库。

我不知道它如何处理增量更改。正如您所说,在编写代码时,它有90%的时间是无效的语法,并且每当空闲时重新解析所有内容都会给CPU带来沉重的负担,而带来的好处却很小,尤其是在修改包含大量的源文件。

我怀疑它是(a)仅在您实际构建项目时才进行解析(或者可能在您关闭/打开它时),或者(b)在某种形式的本地解析中仅在您刚刚解析的地方解析代码以某种有限的方式进行编辑,只是为了获得相关符号的名称。由于C ++具有非常复杂的语法,因此如果您使用繁重的模板元编程等,它在黑暗的角落可能会表现出奇怪的表现。


尝试是一个非常好的主意。对于增量更改,可能无法解决,请先尝试重新解析该文件。如果所有其他方法均失败,请使用最后一个数据库。
stribika

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.