Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

IntelliJ IDEA Scala插件引入增量高亮模式,大幅提升编辑器响应速度

#intellij idea#scala#代码高亮#性能优化#ide开发

如果一个文件中的错误被检测到了,但没有人看到它,那么它真的被高亮显示了吗?

这是一个关于为何“仅高亮显示可见内容”既显得古怪又合情合理的故事,以及你将如何从中受益。

(在“设置 | Scala | 编辑器 | 高亮模式 | 增量”中启用该功能,以加快高亮显示速度并降低资源消耗。)

编译高亮

IDE 和编译器都会分析代码,但它们的方式不同。例如,编译器会:

  • 将源代码转换为可执行代码。
  • 处理每一个源文件。
  • 将错误视为实现目标过程中的障碍。

增量“编译器”可以防止重复编译,但它们最终还是会编译所有内容。

相比之下,IDE 会:

  • 为了理解和编辑的目的而分析代码。
  • 在当前任务的上下文中高亮显示错误。
  • 只处理必要的内容,并且可以容忍错误。

如果一个源文件无法编译,编译器就不会处理依赖于它的其他文件。然而,IDE 可以高亮显示依赖于错误文件的代码,而这些被高亮显示的代码本身可能并没有错误。

到目前为止,一切都很顺利。然而,即使 IDE 的算法不依赖于文件,用户界面却依赖于文件。正如你运行 scalac Foo.scala 一样,你会在编辑器标签页中打开 Foo.scala。IDE 会处理整个文件,即使它非常大,且其中大部分内容是不可见或与当前任务无关的。正因如此,高亮显示取决于你放置类和方法的位置。如果你将 class Fooclass Bar 放在不同的文件中,当你编辑 Bar 时,Foo 不会被高亮显示;但如果两个类在同一个文件中,查看或编辑其中一个类也会高亮显示另一个。

对于编译器而言,处理整个文件的用户界面是自然的,因为它们的工作原理就是如此。然而,IDE 并没有这种限制。与编译器不同,IntelliJ IDEA 可以只对文件的一部分进行重新高亮显示,而无需重新计算所有内容。尽管如此,仍然存在初始高亮显示的过程,且并非每次修改都能局部化。IDE 会保持整个文件处于高亮状态,就像编译器保持整个项目处于编译状态一样——但这并非必须。如果我们能做得更好呢?

文件是一个谎言

考虑以下情况:

[LOADING...]

看起来像一段代码片段,对吧?但其实不然,这是一个“文件”:

[LOADING...]

我们将这种 UI 模式视为理所当然。然而,如果你仔细想想,这样的“文件”与“项目”视图中的“文件”并无区别:标题显示名称,就像节点一样;滚动条提供导航,就像树形结构一样;错误条可以显示标记,树形结构也可以显示标记。我们并没有真正看到整个文件,只看到了一个代码片段。

因此,是否高亮显示整个文件的问题毫无意义——我们无法高亮显示不可见的元素,就像我们无法高亮显示未打开的文件中的元素一样。(在“项目”视图中“打开一个目录”并不能改变这一点。)注意,我们并不说编译器“高亮显示”文件,只有 IDE 才会这样做。我们只能高亮显示可见的内容。在此之外,涉及的是分析,而非高亮显示。

所见即所得

现在,IDE 应该分析多少内容?尽管不可能在高亮显示可见区域之外的代码,但完全可以在错误条上绘制标记、在“项目”视图中为节点添加下划线,或者在“问题”工具窗口中显示错误。IntelliJ IDEA 确实做到了这一切。然而,它不会分析未打开文件中的代码。为什么?分析得更多不是更好吗?(原则上,IDE 可以像编译器一样处理每一个文件。)首先,边际收益递减。其次,这并非没有代价。

源代码不是随机符号的集合。代码具有高内聚低耦合的特性。代码中两点之间的距离越远,它们的关联度就越低。这就是为什么在直接上下文中高亮显示代码更重要,而高亮显示远处的代码重要性较低。此外,后者只会分散对当前任务的注意力。如果你正在编辑一个方法,你肯定不希望在编辑过程中被其他文件中成千上万的错误所干扰。

同时,分析过程会消耗 CPU、内存和电池。它可能会导致 IDE 甚至整个操作系统响应变慢。在这种高亮显示经济学中,你希望通过平衡收益和成本来实现利润最大化。但达到这种“金发女孩效应”(即刚刚好)的合适范围是什么?“文件”是答案吗?

首先,“文件”不是语言实体(编译单元不一定是文件)。它实际上是我们存储源代码方式的一种实现细节。原则上,我们可以将代码作为抽象语法树 (AST) 存储在数据库中;那样的话就不会有文件了。诸如圈复杂度之类的指标是针对包、类和方法的,而不是文件。代码中的距离可能比文件边界更重要。

关于文件的另一个问题是它们可以无限大。原则上,你可以将所有项目类放入一个文件中。这不会影响字节码,但会增加高亮显示源代码的成本。(虽然检测整个文件中的错误有一定好处,但这并不能保证完全没有错误——要做到这一点,你终究需要编译整个项目。)

现在考虑可见区域:这是我们真正可以高亮显示代码的地方。它是注意力的焦点和光标所在的位置,反馈是直接的而非间接的。分析可见区域代码的收益最大。同时,可见区域自然受限于显示分辨率、人类视觉和理解力,且分析成本不依赖于文件大小。“可见区域”可能是一个比“文件”更好的作用域选择。(我们也可以将此逻辑扩展到折叠代码,跳过那些已折叠且实际上不可见的部分。)

???

幸运的是,这不仅仅是一个理论——你现在就可以在实践中尝试它!现在可以在“设置 | Scala | 编辑器 | 高亮模式”中启用增量 (Incremental) 高亮显示:

[LOADING...]

该设置仅适用于 Scala,且仅在你使用内置高亮显示(而非编译器)时可用。该设置为项目级设置,因此如果你想在多个项目中使用此模式,则需要在每个项目中分别启用它。(此功能实际上自 2024.3 版本起就已提供,但我们建议使用最新版本以获得更多改进。)

当启用增量高亮显示时,仅高亮显示可见区域中的代码(排除折叠部分)。该算法可以处理多个编辑器,包括分屏编辑器和差异查看器。

该模式被称为“增量”而非“部分”,因为即使只高亮显示了文件的一部分,已经高亮显示的部分仍将保持高亮。如果你在代码中来回滚动,计算结果会被缓存而不会重复。此外,该模式在修改时的增量更新表现良好;在局部范围内编辑代码会保留已经高亮显示的范围,即使在可见区域之外也是如此。

为了使滚动更平滑,该算法会预先高亮显示可见区域前后各 15 行的内容。(你可以通过调整注册表中的 scala.incremental.highlighting.lookaround 来自定义此范围。)但是,如果你直接导航到文件中的某个定义,可能会观察到即时高亮显示,类似于第一次打开文件或导航到其他文件中的定义时的情况。

错误条(滚动条上的标记)经过过滤,仅包含可见范围以内的数据。下一个/上一个高亮错误功能将在已知(通常是可见的)错误之间导航。一些检查功能(如未使用的声明未使用的导入)处于非活动状态。不过,导入通常无论如何都是折叠的,且优化导入功能是可以正常工作的。这些限制大多是偶然的,旨在简化实现,未来更新中可以改进。(在“设置 | 语言 | Scala | 更新”中选择Nightly 以更快获取更新。)

启用增量高亮模式后,你可以双击 Esc 键按需分析整个文件,并使用所有检查功能。

收益

使用增量高亮显示的好处包括:

  1. 更好的响应速度
  2. 优化的 CPU 使用率
  3. 高效的内存使用
  4. 更低的系统温度
  5. 更安静的运行
  6. 更长的电池续航

这适用于初始高亮显示和重新高亮显示——无论是查看还是编辑代码时。在许多情况下,高亮显示时间可以减少多达 5-10 倍,具体数值取决于文件大小和代码复杂度。增量高亮显示的好处对于 Scala 代码尤为明显,因为 Scala 代码可能相当复杂且难以分析。

收益也取决于硬件;如果你拥有一台带水冷的强大台式机,效果可能不太明显,但如果你使用超极本,差异会显著得多。

欢迎反馈

无论想法还是实现都仍在不断完善中。许多部分还可以进一步优化和改进。(有关更多技术细节,请参阅 SCL-23216。)

我们非常希望你能尝试该功能并分享你的反馈。请通过 YouTrack 报告任何问题。如果你有任何疑问,欢迎随时在 Discord 上向我们提问。

祝开发愉快!

JetBrains Scala 团队