My External Storage

Jul 24, 2020 - 8 minute read - Comments - vim go

vim-goを使わず、LSP(gopls)を使ってVimのGo開発環境を構築する

2020年にもなったので、vim-goを卒業して、vim-lsp(gopls)を使ったVimの開発環境を構築する。

TL;DR

  • vim-goを卒業してgoplsとvim-lspを使った開発環境を構築する
  • VimでLSP(とその他プラグイン)を使えば以下のことができる
    • リアルタイムで静的解析の結果をエディタ上に反映する
    • ポップアップで静的解析のエラーを表示する
    • ポップアップで関数定義などのコメントを表示する
    • 定義元へジャンプができる。
    • package名.などを入力IDEのような補完候補が表示さえる
    • funcと入力してタブを押下するとスニペットが展開される。
    • &http.Client{}と書いたあと:LspCodeActionで構造体のフィールドをゼロ値で初期化する
    • importをよしなに解決する(goimport
    • :wによる自動ソースコード整形、およびそのエラー表示
    • Vim上からテストを実行する
    • Vim上からdelveを使ってデバッグを行う
  • 設定方法などをまとめた

実行環境は次の通り。

$ vim --version
VIM - Vi IMproved 8.2 (2019 Dec 12, compiled May 19 2020 22:07:34)
macOS version
Included patches: 1-800
$ gopls version
golang.org/x/tools/gopls 0.4.3
    golang.org/x/tools/gopls@v0.4.3 h1:irz7Q+XdHNECamFKbNWKvMV2Ak6zBbwdwbZndG4545I=

ひとまず動かしてみたいならば、以下のファイルを利用すれば同様の環境が構築できる(一部pythonライブラリなどの依存解決が必要)。

静的解析の結果を表示したり、 静的解析結果を表示

補完を出したり十分開発に耐えうると思う。 補完

vim-goは?

タイトルの通り、vim-goは今回使わないことにした。

vim-goは少し前なら「vim-go入れればVimのGoの開発環境構築はおしまい!」みたいなプラグインだった。 しかし、vim-go自体が少し微妙な立ち位置になってきたり、「vim-go経由でLSP使うならばもうLSPを直接使えばいいのでは?」というのが2020年07月現在のステータスだと思っている。

詳しい経緯はこのへんの記事が詳しい。

なので、今回はゼロからvim-goを使わないVimのGoの開発環境構築を行なった。

プラグインマネージャの設定

まずはプラグインの自動インストール・アップデートをするためにプラグインマネージャを導入する。 なんとなく通ぶってdein.vimを使うことにした。

dein.vimのインストール設定は@gorilla0513さんの記事に記載されている設定をほぼそのまま使う。

次の設定をvimrcファイルに記載する。

let s:dein_dir = expand('~/.cache/dein')
let s:dein_repo_dir = s:dein_dir . '/repos/github.com/Shougo/dein.vim'

if &runtimepath !~# '/dein.vim'
  if !isdirectory(s:dein_repo_dir)
    execute '!git clone https://github.com/Shougo/dein.vim' s:dein_repo_dir
  endif
  execute 'set runtimepath^=' . s:dein_repo_dir
endif

if dein#load_state(s:dein_dir)
  call dein#begin(s:dein_dir)

  if !has('nvim')
    call dein#add('roxma/nvim-yarp')
    call dein#add('roxma/vim-hug-neovim-rpc')
    " for vim-delve
    call dein#add('Shougo/vimshell.vim')
    call dein#add('Shougo/vimproc.vim', {'build' : 'make'})
  endif

  let s:rc_dir = expand('~/.vim')
  if !isdirectory(s:rc_dir)
    call mkdir(s:rc_dir, 'p')
  endif
  let s:toml = s:rc_dir . '/dein.toml'

  call dein#load_toml(s:toml, {'lazy': 0})

  call dein#end()
  call dein#save_state()
endif

if dein#check_install()
  call dein#install()
endif

let s:removed_plugins = dein#check_clean()
if len(s:removed_plugins) > 0
  call map(s:removed_plugins, "delete(v:val, 'rf')")
  call dein#recache_runtimepath()
endif

if !has('nvim')の節は通常のvimでは一部追加のプラグインを入れないと後述のプラグインが動かないため。 また、pythonのモジュールにも依存しているため、次のコマンドでインストールを済ませておくこと。

$ pip3 install --user pynvim
$ pip3 install --user neovim

あとは~/.vim/dein.tomlにプラグインの依存を書き始めることができる。依存プラグインはVim起動時に自動インストールされる。

ただ、dein.vimを使っているが、がっつりdein.vimの使いかたを覚えるわけでもないので、vim-plugでもよかったかもしれない。
ソフトウェアデザイン8月号のコラムによると、vim-plugのほうがお手軽のようだ。

LSPの設定

何はさておきLSPの設定をする。 先ほどの設定ならば、~/.vim/dein.tomlを読み込むようになっているのでTOMLファイルを作成し、次の依存を書く。

[[plugins]]
repo = 'prabirshrestha/asyncomplete.vim'

[[plugins]]
repo = 'prabirshrestha/asyncomplete-lsp.vim'

[[plugins]]
repo = 'prabirshrestha/vim-lsp'

[[plugins]]
repo = 'mattn/vim-lsp-settings'

[[plugins]]
repo = 'SirVer/ultisnips'

[[plugins]]
repo = 'honza/vim-snippets'

[[plugins]]
repo = 'thomasfaingnaert/vim-lsp-snippets'

[[plugins]]
repo = 'thomasfaingnaert/vim-lsp-ultisnips'

[[plugins]]
repo = 'hrsh7th/vim-vsnip'

[[plugins]]
repo = 'hrsh7th/vim-vsnip-integ'

GoのLSPサーバとしては次の2つを利用する。

goplsはGoチームがメンテしている公式LSPだ。これを使うと補完や定義元ジャンプなどができるようになる。 加えてgolangci-lint-langserverを利用すればリアルタイムで静的解析結果をVim上で確認できる。

あとはvimrcに次のような設定をかく。これはだいたいmattnさんの設定を参考にした。

function! s:on_lsp_buffer_enabled() abort
  setlocal omnifunc=lsp#complete
  setlocal signcolumn=yes
  nmap <buffer> gd <plug>(lsp-definition)
  nmap <buffer> <C-]> <plug>(lsp-definition)
  nmap <buffer> <f2> <plug>(lsp-rename)
  nmap <buffer> <Leader>d <plug>(lsp-type-definition)
  nmap <buffer> <Leader>r <plug>(lsp-references)
  nmap <buffer> <Leader>i <plug>(lsp-implementation)
  inoremap <expr> <cr> pumvisible() ? "\<c-y>\<cr>" : "\<cr>"
endfunction

augroup lsp_install
  au!
  autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
command! LspDebug let lsp_log_verbose=1 | let lsp_log_file = expand('~/lsp.log')

let g:lsp_diagnostics_enabled = 1
let g:lsp_diagnostics_echo_cursor = 1
" let g:asyncomplete_auto_popup = 1
" let g:asyncomplete_auto_completeopt = 0
let g:asyncomplete_popup_delay = 200
let g:lsp_text_edit_enabled = 1
let g:lsp_preview_float = 1
let g:lsp_diagnostics_float_cursor = 1
let g:lsp_settings_filetype_go = ['gopls', 'golangci-lint-langserver']

let g:lsp_settings = {}
let g:lsp_settings['gopls'] = {
  \  'workspace_config': {
  \    'usePlaceholders': v:true,
  \    'analyses': {
  \      'fillstruct': v:true,
  \    },
  \  },
  \  'initialization_options': {
  \    'usePlaceholders': v:true,
  \    'analyses': {
  \      'fillstruct': v:true,
  \    },
  \  },
  \}

" For snippets
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<tab>"
let g:UltiSnipsJumpBackwardTrigger="<s-tab>"

set completeopt+=menuone

上記の設定をして、mattn/vim-lsp-settingsを使っていれば、LSPのインストールは簡単だ。 適当なGoのファイルをvimで開いて、:LspInstallServerをすればよしなにインストールされる・ これでひとまずいい感じにGoが書けるようになる。主要なところを言うと次のような操作ができるようになる。

  • リアルタイムで静的解析の結果をエディタ上に反映する
  • ポップアップで静的解析のエラーを表示する
  • ポップアップで関数定義などのコメントを表示する
  • 定義元へジャンプができる。
  • package名.などを入力IDEのような補完候補が表示さえる
  • funcと入力してタブを押下するとスニペットが展開される。
    • vim-lsp-snippetsなどを導入しているからできる
  • &http.Client{}と書いたあと:LspCodeActionで構造体のフィールドをゼロ値で初期化する

Fill Struct機能は2020年7月現在設定を有効にしないと動作しない。 goplsで可能な操作、機能は次のドキュメントを見ると(メンテが完璧ではないため、)だいたい確認できる。

そして次のような機能は2020年7月現在LSPでは実現できないので、別のプラグインを使う。

  • importをよしなに解決する(goimport
  • :wによる自動ソースコード整形、およびそのエラー表示
  • Vim上からテストを実行する
  • Vim上からdelveを使ってデバッグを行う

ファイル保存時の自動フォーマット、goimports

次のような機能はLSPではまだ実現していないので、別のプラグインを入れる。

  • importをよしなに解決する(goimport
  • :wによる自動ソースコード整形、およびそのエラー表示

とくに追加の設定は必要なく、プラグインへの依存をTOMLに追加するだけだ。

[[plugins]]
repo = 'mattn/vim-goimports'

Vim上からテストを実行する

2020年7月現在LSPからGoのテストは実行できない。 そのため、次のプラグインを導入して、vimからテストを実行できるようにする。

Vimのプラグインではないが、テスト結果が見やすくなるため、私はgotestを使っている。

let test#strategy = "dispatch"
let test#go#runner = 'gotest'

nmap <silent> t<C-n> :TestNearest<CR>
nmap <silent> t<C-f> :TestFile<CR>
nmap <silent> t<C-s> :TestSuite<CR>
nmap <silent> t<C-l> :TestLast<CR>
nmap <silent> t<C-g> :TestVisit<CR>

Vim上からdelveを使ってデバッグを行う

Vimからデバッグを実行することもできる。デバッグにはGoのデファクトであるdelveを使う。 Vim上からブレイクポイントなどを設置して、デバッグを開始できる 先ほどのvim-testと連携させれば、デバッガを使ってテストを実行することもできる。

# tのあとにCTRL+dでテストをデバッガ経由で実行する
function! DebugNearest()
  let g:test#go#runner = 'delve'
  TestNearest
  unlet g:test#go#runner
endfunction
nmap <silent> t<C-d> :call DebugNearest()<CR>

その他のプラグイン

Goに直接関係ないプラグイン関係だと、次のプラグインを導入している。

その他の設定

あとはプラグインではないが、スペルチェックなどをオンにしたりタブの設定をしている。便利。

使っている設定の全容は次の通り。

終わりに

業務でPhpStrom、PyCharmを使うようになったので、Goの実装をするときもGoLandを使うことが多くなった。
ただ、最近leetcode(競プロ)をするようになって「ペライチコードならばやっぱりVimでいいよな」となったのと、メルカリさんのコーディングの実況を見て「ちゃんと設定しよう!」という気持ちになった。

vim-goやGoLandで多用する機能がLSPと各プラグインでほぼフォローできるということがわかったのも大きい。やっぱりVimで書いているときは楽しい。
とはいえ思考のスピードでコーディングできるほどではないので、もっとVim理解しないといけない。

参考

関連記事