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

たぶん1年くらい前だと思うけど、eclimを使ってNeoVimでJavaの開発環境作ったことがある。

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

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

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

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

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

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

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

そもそもさぁ…LSPってなんなの?

LSPとは、Language Server Protocelの略で、2016年6月にMicrosoftが公開した仕様だ。

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

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

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

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

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

今回使用した、最高にイカれたツールを紹介するぜ!

dein.vim

この人を知らないやつはモグリとまで言われている(自分調べ)暗黒美無王ことShougoさん開発のプラグイン管理ツール。
いろんなとこで紹介されているし説明は省略。

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

deoplete.nvim

同じくShougoさん開発のコード補完プラグイン。
こっちも今更語る必要はないだろう。(適当)

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

eclipse.jdt.ls

前述したLSPのJava実装。今回の主役その1。
NeoVimの起動時に、こいつが自動で立ち上がり、後述する LanguageClient-neovim とでメッセージのやり取りを行うことで、NeoVimにIDEの風を吹き込んでくれる憎いやつ。

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

LanguageClient-neovim

前述したLSPのNeoVimクライアント。今回の主役その2。

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

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

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

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

ale.vim

コーディング中に非同期でシンタックスチェックをやってくれるプラグイン。

イメージ画像
ale.vim

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

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

Javaの場合は、標準だと javacgoogle-java-format(後述)1 を使ってシンタックスチェックを行っているらしい。

vim-autoformat

フォーマッターツールを指定してコード整形ができるプラグイン。
ファイルタイプに応じて幾つかデフォルトのフォーマッターが設定されていて、特にフォーマッターの指定がなければそれが実行される。

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

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

google-java-format

Google Java Styleに併せてコードを整形できるCLIツール。

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

そのための設定・・・あと、そのための自動化・・・?

というわけで、プラグインの設定とかの説明。

dein.vimdeoplete.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 で以下の用に定義している。

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 がインストールさているか判定して未インストールの場合はインストール処理を開始するようにしている。

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の起動時などに毎回呼び出される。

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はデフォルト設定のまま使ってる。
シンタックスチェック時のシンボルを変えてるので、一応紹介

dein/dein.toml
[[plugins]]
repo     = 'w0rp/ale'
# プラグイン読み込み時のコールバック
hook_add = 'call hook#add#ale#load()'
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と同じように、アップデート時にフォーマッターのインストール、読み込み時にプラグイン設定を行っている

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()'

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

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

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

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

わーい、うごいたぁ(小並感)

上記の用に設定すると、なんとなくそれっぽく動く用になる。

コード補完と自動フォーマット

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

ドキュメント参照と定義元ジャンプ

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

ほーん。で、何がつらいの?

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

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

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

追記

Gradleプロジェクトでもちゃんと動いた

Gradleプロジェクトの場合、 eclipse plugin が必要だった。

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 タスクを実行してやる。

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

これでGradleプロジェクトでもLSPの機能が使えるようになる。

まとめ

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

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

参考サイト


  1. google-java-formatでのチェックは同ツールがインストール済みの場合のみ実行される。 

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.