好みのエディタに快適な開発環境を提供するLSP


みなさん、エディタは何を使われていますか?
私はEclipseもIntelliJもVisualStudioもviもAtomやVSCodeももろもろ使ってきましたが、毎日orgで色々記録を取ったりと、なんだかんだEmacsが好きです。とはいえ言語によっては他のエディタのほうが効率的なこともあるので、エディタを使い分けるということをしていました。しかし、LSPという平和的なプロトコルの登場によって、エディタ間格差が緩和されつつあるのです。


図1: Emacsのlsp-uiとlsp-goの利用例

LSPに出会ったきっかけは数年前のGoカンファレンス(略して合コン)でした。Sourcegraphの人の発表でサービスを知り、Emacs用の開発支援ツールのベータテストだったりを見ているうちにgo-langserverが公開されていました。

LSPは、発表されてからすでに数年が経過し、今では、C/C++, Go, Java, Pythonなどメジャーな言語に加え、Dockerfileなどの設定管理ファイル、Ballerina1といった言語サーバまで公開されています。図1のように、仕事でも使っているEmacsもLSPに移行しました。本記事では、LSP v3.0の仕組みと、Go言語とLSP、Emacsへの導入方法の3本立てでお送りします。

LSP: Language Server Protocolとは?


図2: LSP導入前後のイメージ

LSPはLanguage Server Protocolの略で、2016年にMicrosoftによって発表された開発支援ツールのためのプロトコルです。
一般的に、プログラミングエディタを用いたソフトウェアの開発では定義部ジャンプ参照箇所の一覧補完などの機能を利用しますが、これらの機能を実現するためには図2の左のように、言語ごと、そしてエディタなどの開発支援ツールごとの実装が必要になります。そのため、エディタ開発者とプログラミング言語開発者の双方に負担がかかるという問題がありました。この問題を解決するために生まれたのがLSPです。

LSPでは、開発支援に必要な機能を提供する言語サーバとそのクライアントのインタフェースを定義しています。よってLSPに則った言語サーバを実装すれば、図2の右図のように(クライアントを実装した)複数のエディタから定義ジャンプなどの機能を同じように2利用することができます。

JSON-RPCによる通信メッセージの種類とカテゴリ


図3: 言語サーバとエディタ間の通信例

言語サーバとクライアント間はJSON-RPCで通信されており、メッセージの通信方式は以下の3つが定義されています。

  1. Notification from Server or Client: ファイルのオープンやクローズをサーバへ通知したり、文法エラーをクライアントへ通知するメッセージ
  2. Request to Server/Response from Server: 定義ジャンプなどのリクエストに対してサーバが定義場所などの回答を返すメッセージ
  3. Request to Client/Response from Client: 開いているワークスペースのリストなどをクライアントから収集するメッセージ

これらの方式をつかって、開発ツールに必要なメッセージがワークスペース、エラーチェックなどの各カテゴリごとに定義されています。全てを紹介するには定義が多すぎるので本家のドキュメントを参照していただくとして、ここではドキュメントの各定義についている矢印の意味だけ説明します。この矢印は、上のメッセージの通信方式を表しています。は1、は2、は3方式であることを示しています。

textDocument/complition

それでは、実際のメッセージの中身をみて理解を深めていきたいと思います。次のコードは、CSSを拡張したlessファイルの編集時に補完機能を呼び出したときのメッセージになっています。リクエストをみると、編集中のファイル名、補完をリクエストしたときのカーソル位置、補完の起動状況の種別が指定されています。CompletionTriggerKindは3種類ありますが、1は識別子のタイプや手動呼び出しなど一般的な補完動作をおこなったときにつかわれる種別です。

client-request.json
textDocument/completion
{
  "textDocument": {
    "uri": "file:///Users/octref/Code/css-test/test.less"
  },
  "position": {
    "line": 1,
    "character": 15
  },
  "context": {
    "triggerKind": 1
  }
}

つづいて、サーバからのレスポンスです。isIncompleteはよくあるpagerのnextのようなもので、trueであれば補完リストの続きを入手することができます。あとに続くItemsには補完アイテムが一つづ格納されています。LSPクライアントは受け取ったこの結果をもとに、ツールチップや何やらを表示することになります。

server-response.json
textDocument/completion
{
  "isIncomplete": false,
  "items": [![test.gif](https://qiita-image-store.s3.amazonaws.com/0/26683/c6210ba8-9f6f-2bda-79a6-5baa464500fa.gif)

    {
      "label": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif",
      "documentation": null,
      "textEdit": {
        "range": {
          "start": {
            "line": 1,
            "character": 15
          },
          "end": {
            "line": 1,
            "character": 15
          }
        },
        "newText": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
      },
      "kind": 12,
      "sortText": "d"
    },
...
}

Go言語の公式toolsにLSPが導入

Goの言語サーバはSourcegraphによって2016年頃から開発が進められていましたが、言語サーバはその言語を使うディベロッパーの体験に大きく寄与するものであり、Goの開発チームによってメンテされたほうが良いということで、今年の秋にtoolsに対してパッチが送られました。Go v1.12あたりから移行が進んでいくかと思います

Goの補完というとgocodeが有名ですが、go-langserverでも補完にgocodeが使われています。lspがtoolsに入ったことで今後どうなっていくかは気になるところです。(詳しい方おしえてください

EmacsとLSP

EmacsのLSPクライアントは、lsp-modeeglotが有名です。eglotが軽量タイプで、lsp-modeが多機能でUIも作り込まれています。個人的にはlsp-uiが気に入っているので基本的にlsp-modeの方を使用しています。lsp-modeで共通的に使われるパッケージは次の3つです。名前から分かりますが、上から順にLSPのクライアントlsp-mode、imenu用のサイドバーやドキュメントやリファレンスのポップアップなどのUIlsp-ui、companyのバックエンドにLSPを使うためのlsp-companyです。

(use-package lsp-mode
  :custom ((lsp-inhibit-message t)
         (lsp-message-project-root-warning t)
         (create-lockfiles nil))
  :hook   (prog-major-mode . lsp-prog-major-mode-enable))

(use-package lsp-ui
  :after lsp-mode
  :custom (scroll-margin 0)
  :hook   (lsp-mode . lsp-ui-mode))

(use-package company-lsp
  :after (lsp-mode company yasnippet)
  :defines company-backends
  :functions company-backend-with-yas
  :init (cl-pushnew (company-backend-with-yas 'company-lsp) company-backends))

言語ごとの個別設定

まずは、LSPを使いたい言語の言語サーバをインストールします。Goの場合はgo-langserverです。忘れずパスも通しておいてください。サーバのインストールを忘れていたりすると*Messages*バッファにエラーがでます。

$ go get -u github.com/sourcegraph/go-langserver

つづいて、言語サーバを起動するためのパッケージをEmacsにインストールします。Goの場合はlsp-goを使います。他の言語もあらかたemacs-lspから探すことができます。ただし、C/C++/Objective-Cについてはcclsがおすすめです。また、go-langserverのデフォルトではlintが無効になっているので、有効にしたい場合はサーバの引数を変更する必要があります。

(use-package lsp-go
  :after (lsp-mode go-mode)
  :custom (lsp-go-language-server-flags '(
    "-gocodecompletion"
    "-diagnostics"
    "-lint-tool=golint"))
  :hook (go-mode . lsp-go-enable)
  :commands lsp-go-enable)

デバッグ方法

クライアント側は*Messages*バッファ、サーバ側はGoだと*lsp-go stderr*バッファから確認することができます。defcustomのlsp-go-language-server-flagsで任意のサーバ起動引数が設定できるので、-pprofと-traceあたりをつけておけば困らないかと思います。

以上、武蔵野アドベントカレンダー4日目の記事でした。

参考文献

  1. LSP official site
  2. JSON-RPC official site
  3. go-review #136676 internal/lsp: the core lsp protocol
  4. GoDoc: golang.org/x/tools/internal/lsp
  5. LSP - Specification
  6. sourcegraph/go-langserver - Go language server
  7. emacs-lsp/lsp-mode - LSP client for Emacs
  8. joaotavora/eglot - Emacs light-weight LSP client
  9. MaskRay/ccls - C/C++/ObjC language server
  10. mdempsky/gocode

  1. Cloud Native Programming Language というCNCF界隈にに突如あらわれた言語。 

  2. 同じ機能を好みのエディタから利用できるため、「このエディタは補完が弱い」ということが無くなる。