Ruby LSP 设计和路线图

设计原则

这些是用于对 Ruby LSP 做出决策的思维模式。

倾向于常见的开发设置

配置开发环境的方式有无数种。不仅可以使用各种工具组合(例如 shell、插件、版本管理器、操作系统等),而且许多工具允许自定义以更改其默认行为。

虽然使用 Ruby 和配置开发环境没有“正确的方法”,但我们必须在 Ruby LSP 可以支持的方面划清界限。尝试考虑每种不同的设置和自定义会分散为更广泛的受众改进体验的努力,并增加长期的维护成本。

示例Ruby on Rails 社区调查报告称,只有 2% 的开发人员未使用版本管理器来安装和配置其 Ruby 版本。虽然每个版本管理器的受欢迎程度各不相同,但可以合理地认为使用版本管理器是使用 Ruby 的常见方式。

基于此,我们将始终

  • 倾向于更常见的开发设置和使用 Ruby 的方式
  • 倾向于默认值和约定,而不是自定义
  • 旨在为常见设置提供零配置体验
  • 在可能的情况下提供灵活性,只要它不影响默认体验

稳定性和性能优先于功能

始终希望添加更完整的编辑器功能或提高正确性。但是,我们将始终优先考虑 Ruby LSP 的稳定性和性能,而不是添加新功能。

即使某个功能有用,或者某个修改提高了现有功能的正确性,如果它会降低性能并对编辑器的响应速度产生负面影响,则实际上可能会导致更差的开发者体验。

示例:常量引用的 Ruby 语法是模棱两可的。仅基于语法无法判断对 Foo 的引用是指类、模块还是常量。因此,我们开始将语义高亮显示功能考虑为所有常量引用作为命名空间,这是最能代表这三种可能性的可用令牌类型。

为了提高高亮显示的正确性,Ruby LSP 必须解析引用以找出它们指向哪个声明,以便我们可以分配正确的令牌类型(类、命名空间或常量)。但是,语义高亮显示会在每次按键时执行,并且解析常量引用是一项开销很大的操作,这可能会导致编辑器出现延迟。我们可能会故意决定不纠正此行为,以保持响应速度。

准确性、正确性和类型检查

Ruby LSP 不附带类型系统。它会执行具有一定级别类型检查的静态分析,但在需要类型注释的情况下会回退到内置的启发式方法。

这意味着它将在可能的情况下提供准确的结果,并在需要完整类型系统的情况下回退到更简单的行为,将决策委托给用户。此外,性能优先于功能也决定了准确性。我们可能更倾向于显示选项列表让用户决定,而不是增加实现的复杂性或降低整体 LSP 性能。

如果您的编辑器需要更高的准确性,请考虑采用类型系统和类型检查器,例如 SorbetSteep

这适用于多种语言服务器功能,例如转到定义、悬停、完成和自动重构。请考虑以下示例

并非所有以下示例目前都受支持,这不是一个详尽的列表。请查看长期路线图以了解计划内容

# Cases where we can provide a satisfactory experience without a type system

## Literals
"".upcase
1.to_s
{}.merge!({ a: 1 })
[].push(1)

## Scenarios where can assume the receiver type
class Foo
  def bar; end

  def baz
    bar # method invoked directly on self
  end
end

## Singleton methods with an explicit receiver
Foo.some_singleton_method

## Constant references
Foo::Bar

# Cases where a type system would be required and we fallback to heuristics to provide features

## Meta-programming
Foo.define_method("some#{interpolation}") do |arg|
end

## Methods invoked on the return values of other methods
## Not possible to provide accurate features without knowing the return type
## of invoke_foo
var = invoke_foo
var.upcase # <- not accurate

## Same thing for chained method calls
## To know where the definition of `baz` is, we need to know the return type
## of `foo` and `bar`
foo.bar.baz

示例:当使用重构功能时,系统可能会提示您确认代码修改,因为它可能不正确。或者,当尝试转到方法的定义时,系统可能会提示您使用与方法调用的名称和参数匹配的所有声明,而不是直接跳转到正确的声明。

作为另一种回退机制,我们希望探索使用变量或方法调用名称作为类型提示来辅助提高准确性(尚未实现)。例如

# Typically, a type annotation for `find` would be necessary to discover
# that the type of the `user` variable is `User`, allowing the LSP to
# find the declaration of `do_something`.
#
# If we consider the variable name as a snake_case version of its type
# we may be able to improve accuracy and deliver a nicer experience even
# without the adoption of a type system
user = User.find(1)
user.do_something

可扩展性

为了减少 Ruby 生态系统中的工具碎片化,我们正在尝试为 Ruby LSP 服务器添加一个附加组件系统。这允许其他 gem 增强 Ruby LSP 的功能,而无需编写完整的语言服务器,从而避免处理文本同步、实现仅依赖于语法的特性(例如折叠范围)或关心编辑器的编码。

我们认为,工具生态系统的碎片化程度较低可以带来更好的用户体验,减少配置并整合社区的努力。

我们的目标是允许 Ruby LSP 连接到不同的格式化程序、linter、类型检查器,甚至是从运行的应用程序(如 Rails 服务器)中提取运行时信息。您可以在附加组件文档中了解更多信息。

依赖 Bundler

了解 Ruby LSP 所用项目的依赖关系,可以为用户提供零配置体验。它可以自动找出哪些 gem 必须被索引才能提供诸如转到定义或完成之类的功能。这还允许它连接到正在使用的格式化程序/linter,而无需请求任何配置。

为了实现这一点,Ruby LSP 依赖于 Ruby 的官方依赖管理器 Bundler。此决定允许 LSP 轻松获取有关依赖关系的信息,但也意味着它受 Bundler 工作方式的约束。

示例:gem 需要安装在项目使用的 Ruby 版本上,以便 Ruby LSP 找到它(需要满足 bundle install)。它需要是相同的 Ruby 版本,否则 Bundler 可能会解析为这些依赖项的不同版本集,这可能会导致由于版本约束而安装失败或 LSP 索引错误的 gem 版本(这可能会导致显示项目中使用的版本中不存在的常量)。

示例:如果我们尝试在没有项目 bundle 上下文的情况下运行 Ruby LSP,那么我们将无法从中需要 gem。Bundler 只将当前 bundle 的依赖项添加到加载路径。忽略项目的 bundle 将使 LSP 无法需要 RuboCop 及其扩展之类的工具。

基于此,我们将始终

  • 依靠 Bundler 提供依赖关系信息
  • 将我们的努力集中在 Bundler 集成上,并帮助改进 Bundler 本身
  • 只有在不通过 Bundler 损害默认体验的情况下,才支持其他依赖管理工具

长期路线图

此路线图的目标是让大家了解我们为 Ruby LSP 计划的内容。这不是一个详尽的任务列表,而是我们希望实现的大型里程碑。

请注意,我们不保证条目的实现顺序,也不保证它们是否会完全实现,因为我们可能会在此过程中发现障碍。

有兴趣参与贡献?请查看带有 help-wantedgood-first-issue 标签的问题。