小编典典

代码完成如何工作?

algorithm

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

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

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

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


阅读 419

收藏
2020-07-28

共1个答案

小编典典

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

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

  1. 在光标处获取表达式。从 成员访问表达式 的开头到光标所经过的标识符的结尾。成员访问表达式通常采用形式aa.bb.cc,但也可以包含方法调用(如中)aa.bb(3+2).cc
  2. 获取光标周围的 上下文 。这非常棘手,因为它并不总是遵循与编译器相同的规则(长话短说),但是在这里假设它遵循。通常,这意味着获取有关游标所在的方法/类的缓存信息。
  3. 假设上下文对象实现IDeclarationProvider,您可以在其中调用GetDeclarations()以获取IEnumerable<IDeclaration>作用域中所有可见项的。就我而言,此列表包含局部变量/参数(如果在方法中),成员(字段和方法,仅静态(除非在实例方法中,除非是实例方法且没有基本类型的私有成员),全局变量(语言I的类型和常量)正在开发)和关键字。该列表中将包含一个名称为的项目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#中)
  • 语义正确查找所有参考
2020-07-28