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 連接到不同的格式化工具、程式碼檢查工具、型別檢查器,甚至從正在執行的應用程式(例如 Rails 伺服器)擷取執行階段資訊。您可以在附加元件文件中了解更多資訊。

依賴 Bundler

了解使用 Ruby LSP 的專案的相依性,可以讓它為使用者提供零配置體驗。它可以自動找出哪些 gem 必須被索引,以提供跳至定義或完成等功能。這也讓它可以連接到正在使用的格式化工具/程式碼檢查工具,而無需任何配置。

為了實現此目的,Ruby LSP 依賴於 Ruby 的官方相依性管理器 Bundler。此決定讓 LSP 可以輕鬆取得有關相依性的資訊,但也表示它會受到 Bundler 的運作方式影響。

範例:gem 需要安裝在專案使用的 Ruby 版本上,Ruby LSP 才能找到它(需要滿足 bundle install)。它必須是相同的 Ruby 版本,否則 Bundler 可能會將這些相依性解析為不同的版本集,這可能會導致因版本約束而無法安裝,或者導致 LSP 索引不正確的 gem 版本(這可能會導致顯示專案使用的版本中不存在的常數)。

範例:如果我們嘗試在沒有專案套件內容的情況下執行 Ruby LSP,那麼我們將無法從中要求 gem。Bundler 只會將目前套件中的相依性新增至載入路徑。忽略專案的套件會讓 LSP 無法要求 RuboCop 及其擴充功能等工具。

基於此,我們將始終

  • 依賴 Bundler 提供相依性資訊
  • 將我們的努力集中在 Bundler 整合上,並協助改進 Bundler 本身
  • 只有在不透過 Bundler 損害預設體驗的前提下,才支援其他相依性管理工具

長期路線圖

此路線圖的目標是讓大家了解我們為 Ruby LSP 計劃的內容。這不是詳盡的任務清單,而是我們希望實現的重大里程碑。

請注意,我們無法保證條目將按什麼順序實作,或者是否會實作,因為我們可能會在此過程中發現障礙。

有興趣貢獻嗎?請查看標記為 help-wantedgood-first-issue 的問題。