LSPあるし改めてVim(NeoVim)でJava書く環境作ってみたけどやっぱりIDEには勝てなかったよ…

たぶん1年くらい前だと思うけど、[eclim](http://eclim.org/)を使ってNeoVimでJavaの開発環境作ったことがある。

このeclim、バックグラウンドでEclipseを起動してJava開発環境を提供しているだけあってかなり高性能なんだけどEclipseよろしくワークスペースが必要だったり、バックグラウンドでEclipseが立ち上がるのでVimなのになんか重い、インストールがめんどくさくて自動化しづらい等辛い部分も多々あって、ほとんど使わずに消してしまった。

というわけで、結局Javaを書くときはIDE使ってたんだけど、最近になって[eclipse.jdt.ls](https://github.com/eclipse/eclipse.jdt.ls)というJavaのLSPがあるのを見つけた。

絶賛、開発環境をLSPに置き換えキャンペーン中の僕としては

「eclipseの公式ぷらぎんみたいだし、環境構築自動化できそうだしこれは勝ったな（白痴）」

的な気持ちで環境構築してみた。

結局IDEには勝てなかったわけだけど、割りと自分的には満足できたので紹介して見る。

僕が使ってるのはNeoVimだけど、通常のVimでも同じような感じで環境構築できるはず。


# そもそもさぁ…LSPってなんなの？
LSPとは、`Language Server Protocel`の略で、2016年6月にMicrosoftが公開した仕様だ。

こいつが一体どういうもんなのか言うと、IDEに必要となるコード補完やエラー分析などの機能をサービスとして提供する上での共通仕様が定義されている。

このように仕様を共通化することで、LSPの言語実装があり使用しているエディタにLSPクライアントがあるならばどのような環境であってもIDE機能が利用可能となる。

LSPについてはこちらの記事にわりと詳しく書かれているし、公式サイトやGitHubでも仕様が公開されているので興味があれば見て貰いたい。

* [language server protocolについて (前編) - Qiita](https://qiita.com/atsushieno/items/ce31df9bd88e98eec5c4)
* [Language Server Protocol](https://microsoft.github.io/language-server-protocol/specification)
* [Microsoft/language-server-protocol - GitHub](https://github.com/Microsoft/language-server-protocol)

また、言語ごとの実装やエディタごとのクライアントについて下記でまとめられている。

* [Langserver.org](https://langserver.org/)

今回の環境構築では、このLSPのJava実装である`eclipse.jdt.ls`と、NeoVimのLSPクライアントである`LanguageClient-neovim`を使ってJava開発環境を作るのに挑戦してみたわけだ。


# 今回使用した、最高にイカれたツールを紹介するぜ！
## [dein.vim](https://github.com/Shougo/dein.vim)
この人を知らないやつはモグリとまで言われている（自分調べ）暗黒美無王ことShougoさん開発のプラグイン管理ツール。
いろんなとこで紹介されているし説明は省略。

詳しいことはここらへんを参考にしてほしい。

* [dein.vimを使ってみる - Qiita](https://qiita.com/yoza/items/2f8bd33a18225754f346)
* [neovimをインストールしてプラグイン管理をdein.tomlでやってみた - Qiita](https://qiita.com/ryo2851/items/4e3c287d5a0005780034)

## [deoplete.nvim](https://github.com/Shougo/deoplete.nvim)
同じくShougoさん開発のコード補完プラグイン。
こっちも今更語る必要はないだろう。（適当）

これも詳しいことは別の記事に丸投げする。

* [Vimでの補完ツールプラグインをneocompleteからdeopleteへ - rcmdnk's blog](https://rcmdnk.com/blog/2017/11/16/computer-vim/)

## [eclipse.jdt.ls](https://github.com/eclipse/eclipse.jdt.ls)
前述したLSPのJava実装。今回の主役その１。
NeoVimの起動時に、こいつが自動で立ち上がり、後述する `LanguageClient-neovim` とでメッセージのやり取りを行うことで、NeoVimにIDEの風を吹き込んでくれる憎いやつ。

起動速度もメモリ使用量もeclimよりもだいぶマシで、インストールもとりあえず自動化できる程度に簡単。

## [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim)
前述したLSPのNeoVimクライアント。今回の主役その２。

コード補完、コードフォーマット、シンタックスチェック、ドキュメント表示、定義元ジャンプなどなど、コーディングに最低限必要と思われることはこいつと言語ごとのLSP用意すれば一通りできるようになる、最高にイカした野郎。

自分の場合は、シンタックスチェックとコードフォーマットは別のプラグインにやらせてるんで使ってない。

中の人がMS社員(とどっかで聞いた気がする)が開発してる[vim-lsp](https://github.com/prabirshrestha/vim-lsp)もあるんだけど、deoplete対応してるんでこっち使ってる。
GitHubのスター数もLanguageClient-neovimのが多い。

ちなみに、名前に `neovim` とかついてるけど、普通のvimでも使えるらしい。
試したことはない。

## [ale.vim](https://github.com/w0rp/ale)
コーディング中に非同期でシンタックスチェックをやってくれるプラグイン。

**イメージ画像**
![ale.vim](https://raw.githubusercontent.com/w0rp/ale/master/img/example.gif)

`LanguageClient-neovim` にも同じような機能はあるんだけど、LSP対応してない言語とかはこいつ使ってるんで、統一する目的でこのプラグインを使ってる。

あと自分の好みでチェックツールをカスタマイズできるのも○。

Javaの場合は、標準だと `javac` と `google-java-format(後述)`[^1] を使ってシンタックスチェックを行っているらしい。
[^1]: `google-java-format`でのチェックは同ツールがインストール済みの場合のみ実行される。

## [vim-autoformat](https://github.com/Chiel92/vim-autoformat)
フォーマッターツールを指定してコード整形ができるプラグイン。
ファイルタイプに応じて幾つかデフォルトのフォーマッターが設定されていて、特にフォーマッターの指定がなければそれが実行される。

同じような機能は `ale.vim` にもあるのだが、割りと昔からこのプラグインを使っていてそのながれで未だに使用中。

コード保存時に、後述の `google-java-format` でフォーマットが行われる用に設定する。

## [google-java-format](https://github.com/google/google-java-format)
[Google Java Style](https://google.github.io/styleguide/javaguide.html)に併せてコードを整形できるCLIツール。

デフォルトだと、多くのJavaおじさんたちに馴染みの無いであろう2スペースインデントでフォーマットされる。
`--aosp`オプション(Android Open Source Project)を指定することで4スペースインデントにできるので安心してほしい。


# そのための設定・・・あと、そのための自動化・・・？
というわけで、プラグインの設定とかの説明。

`dein.vim` と `deoplete.nvim` の設定は今回あまり関係ないと思うので省略。

ちなみにディレクトリ構成はこんな感じ。

```
$XDG_CONFIG_HOME/nvim
  ├── autoload
  │   └── hook
  │       ├── add
  │       │   ├── ale.vim
  │       │   ├── language_client_neovim.vim
  │       │   └── vim_autoformat.vim
  │       ├── post_update
  │       │   ├── ale.vim
  │       │   ├── language_client_neovim.vim
  │       │   └── vim_autoformat.vim
  │       └── source
  │           └── deoplete.vim
  ├── dein
  │   ├── dein.toml
  │   └── dein_lazy.toml
  └── init.vim
```


## LanguageClient-neovim周りの設定
まずは、目玉の `LanguageClient-neovim` の設定からだ。
`dein.toml` で以下の用に定義している。

```toml:dein/dein.toml
[[plugins]]
repo             = 'autozimu/LanguageClient-neovim'
rev              = 'next'
# プラグインのアップデート時に呼び出されるコールバック
hook_post_update = 'call hook#post_update#language_client_neovim#load()'
# プラグインが読み込まれる際に呼び出されるコールバック
hook_add         = 'call hook#add#language_client_neovim#load()'
```

アップデート時と読み込み時に2つの関数を呼び出して設定などを行っている。

プラグインのアップデート時は、`eclipse.jdt.ls` がインストールさているか判定して未インストールの場合はインストール処理を開始するようにしている。

```vim:autoload/hook/post_update/language_client_neovim.vim
function! hook#post_update#language_client_neovim#load() abort
  !./install.sh
  " g:outher_package_pathは、`eclipse.jdt.ls`などの外部ツールのインストール先ディレクトリ。
  " 省略しているが、`init.vim`で設定している。
  let l:jdt_lsp_path = expand(g:outher_package_path) . "/jdt-lsp"
  " 指定のディレクトリに`eclipse.jdt.ls`が存在するか確認
  if !executable(l:jdt_lsp_path . "/plugins/org.eclipse.equinox.launcher_1.5.0.v20180207-1446.jar")
    " `eclipse.jdt.ls`のダウンロード
    !curl -o /tmp/tmp_jdt_lsp.tar.gz http://download.eclipse.org/jdtls/snapshots/jdt-language-server-0.16.0-201803280253.tar.gz
    " `eclipse.jdt.ls`の保存先ディレクトリを作成
    call mkdir(l:jdt_lsp_path, "p")
    " ダウンロードしてきたファイルを保存先ディレクトリに解凍
    execute "!tar xf /tmp/tmp_jdt_lsp.tar.gz -C " . l:jdt_lsp_path
    " tar.gzファイルを削除
    !rm /tmp/tmp_jdt_lsp.tar.gz
  endif
endfunction
```

次に読み込み時の設定だ。
LSPの起動設定などを行っていて、NeoVimの起動時などに毎回呼び出される。

```vim:autoload/hook/add/language_client_neovim.vim
function! hook#add#language_client_neovim#load() abort
  let g:LanguageClient_autoStart         = 1 " NeoVim起動時にLSPを自動スタート
  let g:LanguageClient_diagnosticsEnable = 0 " シンタックスチェックをOFF

  let g:LanguageClient_serverCommands = {}
  " `eclipse.jdt.ls`で利用する、データ保存先ディレクトリの存在確認
  " ディレクトリが存在しない場合は作成する
  let l:jdt_lsp_data_dir = expand(g:outher_package_path) . "/jdt-data"
  if !isdirectory(l:jdt_lsp_data_dir)
    call mkdir(l:jdt_lsp_data_dir, "p")
  endif
  " LSPの起動設定
  " `configuration`オプションはOSごとに別の設定にする必要がある。
  " `eclipse.jdt.ls`インストールディレクトリに、 `config_linux`, `config_mac`, `config_win` というディレクトリがあるので、それぞれOSに併せて設定ファイルパスを指定する。
  let g:LanguageClient_serverCommands["java"] = [
        \ 'java',
        \ '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044',
        \ '-Declipse.application=org.eclipse.jdt.ls.core.id1',
        \ '-Dosgi.bundles.defaultStartLevel=4',
        \ '-Declipse.product=org.eclipse.jdt.ls.core.product',
        \ '-Dlog.protocol=true',
        \ '-Dlog.level=ALL',
        \ '-noverify',
        \ '-Xmx1G',
        \ '-jar',
        \ expand(g:outher_package_path) . '/jdt-lsp/plugins/org.eclipse.equinox.launcher_1.5.0.v20180207-1446.jar',
        \ '-configuration',
        \ expand(g:outher_package_path) . '/jdt-lsp/config_mac',
        \ '-data',
        \ l:jdt_lsp_data_dir]

  " キーマッピング
  nnoremap <silent> K :call LanguageClient_textDocument_hover()<CR>
  nnoremap <silent> gd :call LanguageClient_textDocument_definition()<CR>
  nnoremap <silent> <F2> :call LanguageClient_textDocument_rename()<CR>
  nnoremap <silent> <F3> :call LanguageClient_textDocument_references()<CR>
endfunction
```

## ale.vim
Javaに関しては、ale.vimはデフォルト設定のまま使ってる。
シンタックスチェック時のシンボルを変えてるので、一応紹介

```toml:dein/dein.toml
[[plugins]]
repo     = 'w0rp/ale'
# プラグイン読み込み時のコールバック
hook_add = 'call hook#add#ale#load()'
```

```vim:autoload/hook/add/ale.vim
function! hook#add#ale#load() abort
  let g:ale_sign_error      = '✖' " エラーシンボル
  let g:ale_sign_warning    = '⚠' " 警告シンボル
endfunction
```

## vim-autoformat
LanguageClient-neovimと同じように、アップデート時にフォーマッターのインストール、読み込み時にプラグイン設定を行っている

```toml:dein/dein.toml
[[plugins]]
repo             = 'Chiel92/vim-autoformat'
# プラグイン読み込み時のコールバック
hook_add         = 'call hook#add#vim_autoformat#load()'
# プラグインアップデート時のコールバック
hook_post_update = 'call hook#post_update#vim_autoformat#load()'
```

アップデート時のコールバック処理はこんな感じ

```vim:autoload/hook/post_update/vim_autoformat.vim
function! hook#post_update#vim_autoformat#load() abort
  " `google_java_formatter`がインストールさてれているか確認
  let l:google_java_formatter = expand(g:outher_package_path) . "/google-java-format-1.5-all-deps.jar"
  if !executable(l:google_java_formatter)
    " 未インストールの場合はjarをダウンロード
    execute "!wget https://github.com/google/google-java-format/releases/download/google-java-format-1.5/google-java-format-1.5-all-deps.jar -P " . expand(g:outher_package_path)
  endif
endfunction
```

読み込み時の設定はこんなかんじ。
フォーマッタの設定と、保存時の自動フォーマットを設定している。

```vim:autoload/hook/add/vim_autoformat.vim
function! hook#add#vim_autoformat#load() abort
  let g:autoformat_remove_trailing_spaces = 1 " 末尾スペースの除去

  " google_java_formatterの起動コマンド設定
  let g:formatdef_google_java_formatter = '"java -jar ' . g:outher_package_path . '/' . g:google_java_formatter . ' - --aosp"'

  " Javaのフォーマッタの設定
  let g:formatters_java = ['google_java_formatter']

  " 保存時に自動でコードフォーマットされる用に設定
  call s:set_autoformat("java")
endfunction

function! s:set_autoformat(...) abort
  augroup AutoIndentPreWrite
    autocmd!
  augroup End

  for var in a:000
    let l:cmd = 'autocmd AutoIndentPreWrite BufWrite *.' . var . ' :Autoformat'
    execute l:cmd
  endfor
endfunction
```

# わーい、うごいたぁ（小並感）
上記の用に設定すると、なんとなくそれっぽく動く用になる。

**コード補完と自動フォーマット**
<img src="https://qiita-image-store.s3.amazonaws.com/0/36379/4f9ba055-ada8-5ed9-0ce3-26227b501bb6.gif" width="90%">

こんな感じで、Lambda式もしっかりコード補完してくれる。

**ドキュメント参照と定義元ジャンプ**
<img src="https://qiita-image-store.s3.amazonaws.com/0/36379/0eaa4145-7565-1e5e-d517-9f087c55b15d.gif" width="90%" />

標準パッケージとか外部ライブラリへの定義元ジャンプはできなかった。


# ほーん。で、何がつらいの？
1. オートインポートがない
2. インターフェース実装時に、抽象メソッドを自動で実装してくれない
3. ~~Gradleプロジェクトでうまく動かない。~~

主にここらへん。
1, 2はGoとかだとそんな気にならなかったからJavaも行けるやろと思ったけど意外と辛かった。

~~3に関してはMavenプロジェクトだと外部パッケージのコード補完もドキュメント参照もちゃんと動いてるから、なんか設定とかミスってる可能性あるかも…？~~
[追記 - Gradleプロジェクトでもちゃんと動いた](#gradleプロジェクトでもちゃんと動いた)

# 追記
<div id="gradle-tuiki"/>
## Gradleプロジェクトでもちゃんと動いた
Gradleプロジェクトの場合、 `eclipse plugin` が必要だった。

```gradle:build.gradle
plugins {
    id 'java'
    id 'application'
    id 'eclipse'
}

mainClassName = 'App'

dependencies {
    compile 'com.google.guava:guava:23.0'

    testCompile 'junit:junit:4.12'
}

repositories {
    jcenter()
}
```

`eclipse plugin` を適用したらプロジェクトルートで `eclipseJdt` タスクを実行してやる。

```console
$ gradle eclipseJdt
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
```

これでGradleプロジェクトでもLSPの機能が使えるようになる。
<img src="https://qiita-image-store.s3.amazonaws.com/0/36379/ebbf3aa1-9105-06fa-87e9-0bcc67396079.gif" width="90%">


# まとめ
そんなわけで、やっぱりIDEには勝てなかったよ…
とはいえ、IDEよりも手軽にコード編集できて、コード補完もかなり気の利いたもん出してくれるので、eclimのように全く使わずに削除ってことはなさそう。

今つらいと思ってるとこも、慣れてくると割りとなんとかなったりするかもしれないし、もしかしたら知らないだけでなんか便利なツールあるかもしんないので、また暇な時にでも環境見直して見ようと思う。


# 参考サイト
* [language server protocolについて (前編) - Qiita](https://qiita.com/atsushieno/items/ce31df9bd88e98eec5c4)
* [dein.vimを使ってみる - Qiita](https://qiita.com/yoza/items/2f8bd33a18225754f346)
* [neovimをインストールしてプラグイン管理をdein.tomlでやってみた - Qiita](https://qiita.com/ryo2851/items/4e3c287d5a0005780034)
* [Vimでの補完ツールプラグインをneocompleteからdeopleteへ - rcmdnk's blog](https://rcmdnk.com/blog/2017/11/16/computer-vim/)