igsr5 のブログ

呟きたい時に呟きます

Shopify/ruby-lsp の内部実装を読み解く (1) v0.0.1 の変更箇所、LSP の基本動作、LSIF (Language Server Index Format)

概要

個人的に最近アツい https://github.com/Shopify/ruby-lsp の内部実装を読み解いて追加機能の提案・実装が出来るレベルを目指す。

前提

筆者に LSP (Language Server Protocol) の知識はほとんどない。クライアントとして利用しているのでどういったものかは知っているが、その内部実装には明るくない。なのでこの記事を読んでいる人で内容の誤りに気がついたらコメントして頂けるとありがたい。

また Ruby および TypeScript はそれなりに歴があるのでコードを読むことに不自由はない。

やること

https://github.com/Shopify/ruby-lsp のコミットを古い方から読んでいく。こういったコードリーディングはやったことないが、単に最新のソースコードを読むよりも Shopify の優秀なエンジニアの手癖を見ることが出来て学びが多そうなので実践してみる。

なお、あまり上手くいかなかったら違うアプローチに変えるつもり。

本編

今日は初回なので v0.0.1 の commit を読む。

Initial commit · Shopify/ruby-lsp@90afd3e · GitHub

記念すべき initial commit。個人的に興味深かった点。

Add boilerplate LSP server · Shopify/ruby-lsp@873fa35 · GitHub

LSP らしい実装が初めて入った commit。ここで LSP のキャッチアップを済ませておく。

LSP とは

Official page for Language Server Protocol の記載が分かりやすい。

The Language Server Protocol (LSP) defines the protocol used between an editor or IDE and a language server that provides language features like auto complete, go to definition, find all references etc.

まず前提として、Language Server とは特定のプログラミング言語(あるはそれらに準ずる言語)において高度な言語機能、例えば補完やフォーマッタなどの機能を提供するサーバーのことである。例えば今回のテーマである Shopify/ruby-lsp は Language Server の1つである。

そして LSP (Language Server Protocol) とはこの Language Server と開発ツール間の通信プロトコルである。 このプロトコルのおかげで、世のエディタは高度な言語機能を特定言語に依存しない形で統一的に組み込むことができる。

LSP の動作原理

詳細はこの後の Shopify/ruby-lsp のコードリーディングから読み取るとして、ここでは https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/ の内容を基本として抑えておく。

まず LSP はサーバークライアント方式である。このことは次の図を見ると分かりやすい。Development Tool (クライアント) は JSON-RPC を用いて Language Server に Notification あるいは Request を送信する。そうすると Language Server は Development Tool に Notification あるいは Response を返す。

https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/

LSP は JSON-RPC プロトコルに則って定義した rpc を含んでおり、図中でも利用されている代表的な rpc として以下が挙げられる。

  • textDocument/didOpen
    • 新しい textDocument を開いた際にクライアントからサーバーに送信される Notification
  • textDocument/didChange
    • textDocument に変更を加えた際に送信される Notification
  • textDocument/publishDiagnostics
    • サーバーからクライアントへ push されるバリデーション結果の Notification
    • error や warning などの情報を含める
    • 結果のownershipはサーバーにあるのでサーバー側は必要に応じて diagnostics を clear したり merge したりする必要がある
  • textDocument/definition
    • 任意の位置の symbol の定義場所の解決を行いたい際にクライアントからサーバーへ送られる Request
  • textDocument/didClose
    • textDocument を閉じた際にクライアントからサーバーに送信される Notification

同時に2言語以上のファイルを編集する際には以下の図のように複数プロセスのLanguage Server と単一の Development Tools に対してそれぞれ上記の JSON-RPC を発行することになっている。

https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/

また LSP にはある Language Server・Development Tools が提供可能な言語機能のセットを表す Capabilities という概念がある。実装レベルでの話は置いておいて、ここでは前提知識として全ての Language Server や Development Tools が上記 JSON-RPC を網羅的にサポートしているわけではないことを頭に入れておくと良い。

LSIF (Language Server Index Format)

https://microsoft.github.io/language-server-protocol/overviews/lsif/overview/

LSP の関連技術として LSIF ("else if" と発音する) という技術が存在する。この技術は名前の通り LSP で度々計算されるファイル群に対する index を発行する技術で LSP 起動時に indexing files といった表示が出るのはこの LSIF が関連している(中身を見たわけではないので少し自信ない)

詳細な動作原理は後々追うとして、ここでは LSP と LSIF がそれぞれ異なる目的を持った技術であること、LSP によっては必要に応じて LSIF を活用していることを覚えておくと良い。

  • LSP の目的
    • リアルタイムなコード解析
  • LSIF の目的
    • 事前に用意したインデックスを用いたコードナビゲーション

LSIF の活用例: textDocument/hover の応答として毎回コード解析を行わずにインデックスを用いて高速に Response を返す。

ここまでで最低限必要な知識は得られたので、一旦 LSP のキャッチアップはここまでにして Shopify/ruby-lsp のコードリーディングに戻る。

(再開) Add boilerplate LSP server · Shopify/ruby-lsp@873fa35 · GitHub

この commit には以下のコードが含まれる。

# frozen_string_literal: true

require "language_server-protocol"

module Ruby
  module LSP
    module Cli
      def self.start(_argv)
        writer = LanguageServer::Protocol::Transport::Stdio::Writer.new
        reader = LanguageServer::Protocol::Transport::Stdio::Reader.new

        subscribers = {
          initialize: -> {
            LanguageServer::Protocol::Interface::InitializeResult.new(
              capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
                text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
                  change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL
                ),
                completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new(
                  resolve_provider: true,
                  trigger_characters: %w(.)
                ),
                definition_provider: true
              )
            )
          }
        }

        reader.read do |request|
          result = subscribers[request[:method].to_sym].call
          writer.write(id: request[:id], result: result)
          exit
        end
      end
    end
  end

上記コードと利用している third-party SDK のテストコードから分かることは以下の通り。

  • この LSP Server は initialize リクエストに対して capabilities を返すことができる
    • capabilities に含まれるメソッドは以下の通り
    • "capabilities"=>{"textDocumentSync"=>{"change"=>1}, "completionProvider"=>{"triggerCharacters"=>["."], "resolveProvider"=>true}, "definitionProvider"=>true}
    • textDocumentSync.change
    • ただし capabilities に含まれるメソッドは定義こそあれど実装は空なので実際には何もしない
  • reader.read do |request| ~ のプログラムで実際にクライアントから受け取った Request を処理している

さて、ここまで進んでみて、先に GitHub - mtsmfm/language_server-protocol-ruby: A Language Server Protocol SDK for Ruby にも一通り目を通しておくと理解が捗りそうなことに気がついた。この記事も長くなってしまったので今回は一旦終わりにしつつ次回は Shopify/ruby-lsp が内部で利用している mtsmfm/language_server-protocol-ruby のコードリーディングを行おうと思う。

参考資料