LSP学習記 #4 シンボルのリネーム

Qiita

自作言語の LSP サーバーを作るプロジェクトの第4回です。今回はソースコードを変更する機能の例として、シンボルのリネームを実装してみました。

名前の変更

変数などの名前を変更するとき、単純な文字列置換では「同名だが異なる変数」といったものまで巻き添えにしてしまいます。安全に変更するには、前回のようにシンボル情報を解析しておくほうがよいです。そういうわけで、LSP サーバーの機能に「名前の変更」があります。

具体的には、 LSP クライアントは textDocument/rename リクエスト で、どの位置にあるシンボルをどんな名前に変えるべきかという情報をサーバーに送信してくれます。

このとき、具体的なソースコードの変更点を生成してレスポンスすれば、名前の変更ができるようです。

  • 注: 前回と同様に、LSP クライアントからのリクエストをもらうには、サーバーの capabilitiestextDocument.renameProvider: true の指定が必要です。

名前の変更: 変更操作の表現

ソースコードに対する変更は TextEdit インターフェイスで定義されていて、「ある範囲の文字列を別の文字列で置換する」ような形式です。 Array.splice 方式。

例えば次の文字列の範囲 [0, 4] .. [0, 5] (x の部分) を文字列 “new_x” で置換する、みたいな感じです。

let x be 1
let new_x be 1

いまのクラゲ言語は1つのファイルにしかソースコードを書けませんが、一般には名前の変更は複数のファイルを変更することになります。rename レスポンスで返すべきオブジェクトは、 WorkspaceEdit というインターフェイスで定義されていて、ファイルの URI から変更操作へのマップのようなものです。

interface WorkspaceEdit {
    // URI から変更操作の配列へのマップ
    changes?: { [uri: string]: TextEdit[]; };

    // 以下略
}

実装

実装は、前回作ったシンボルテーブルを利用すれば簡単です。

前回は「ドキュメントのハイライトする範囲」を計算しましたが、今回はそれを「名前の変更を適用する範囲」として使えばOK。

  const { definition, references } = symbolDefinition // ヒットテストで見つけたシンボル

  // 変更操作の配列
  const textEdits: TextEdit[] = []

  // 定義の置換
  textEdits.push({
    range: definition.range,
    newText: newName,
  })

  for (const r of references) {
    // 参照の置換
    textEdits.push({
      range: r.range,
      newText: newName,
    })
  }

  // WorkspaceEdit インターフェイスに合うオブジェクトを作る
  const changes = { [uri]: textEdits }
  return { changes }

prepareRename

textDocument/prepareRename リクエストという、 rename の前に送られてくるリクエストがあります。名前の変更ができない位置 (例えば let キーワードの上) では prepareName の返り値として null を返すことで、名前の変更が不可能であることをクライアントに伝えられる……らしいんですが、実装してみても効果が見られなかったので詳細は略。

TextDocumentEdit

LSP の仕様をよく読むと WorkspaceEdit.changes ではなく documentChanges を使ったほうがよいみたいです。

LSP サーバーが処理をしている間にも、ドキュメントはユーザーによって絶え間なく変更されているので、同じドキュメントにも古いバージョンと新しいバージョンがあります。名前の変更がどのバージョンを処理したのかを指定すると、クライアント側が嬉しいらしいです。

WorkspaceEdit.documentChanges には TextDocumentEdit (の配列) を指定しますが、これはドキュメントの URI だけでなくバージョンも指定した変更操作を表しています。

注意: 安全でない変更

今回の実装では、場合によってはコードの意味を変えてしまいます。例えば次のコードの xy という名前に変えると、2つ目の x が途中に挟まってる y を指すものになってしまいます。これは本来ならユーザーに警告したほうがよいです。

let x be 1
let y be 2
let _ be x

次回

次回は未定です。そろそろ簡単な計算のできる言語にしつつ、入力補完やホバーあたりをやっていこうかと考えています。

関連記事