【解説】開発ライブ実況 #1 (Vim / Go) 編 by メルペイ Architect チーム Backend エンジニア #mercari_codecast

Merpay Architect / Mercari Microservices Platform チームの伊藤です。この記事は Merpay Tech Openness Month の3日目の投稿となります。本稿では、先日開催した開発ライブ実況のイベントで紹介した筆者の開発環境(Vim / Go)について、言語に依存しない「全般的な設定」と「Goの設定」の2つに大別して解説します。Vim に関する話題が多いですが、Go のために自作したツールについての解説はエディタに依存しないので、他のエディタを利用している方々もぜひご一読ください。

開発ライブ実況とは

「他人の開発風景を覗いてみよう!」というコンセプトのもとに弊社が開催しているコーディング実況イベントです。このイベントは、開発者が事前に課題(TODOアプリのサーバサイド等)に対するコーディングを録画し、その動画を他の開発者と見ながら実況放送する、というものです。 このイベントの第1回目で、筆者が Vim(Neovim)を使って Go でサーバを開発する風景を実況しました。実況動画のアーカイブはこちらになります。

全般的な設定

Fuzzy Finder

Fuzzy Finder としては fzf と、その Vim プラグインである fzf.vim を利用しています。 fzf はエディタに依らない汎用的な CLI の Fuzzy Finder です。下図は fzf と Rust で実装された高速な grep ツールである ripgrep を連携して、プロジェクトのファイル全体から特定の文字列を検索し、そのファイルにジャンプする例です。

fzfでファイル検索を行う例

上図の操作は下記のような設定を用いて行っています。

nnoremap <silent> <Leader>g :<C-u>silent call <SID>find_rip_grep()<CR>

function! s:find_rip_grep() abort
    call fzf#vim#grep(
                \   'rg --ignore-file ~/.ignore --column --line-number --no-heading --hidden --smart-case .+',
                \   1,
                \   fzf#vim#with_preview({'options': '--delimiter : --nth 4..'}, 'right:50%', '?'),
                \   0,
                \ )
endfunction

fzf.vim がデフォルトで提供しているファイル一覧や grep 以外にも、ブランチ一覧やプロジェクト一覧、シンボルの絞り込みなど、何かを絞り込んで検索したいときには全て fzf を経由するように設定しています。

Language Server Protocol クライアント

Language Server Protocol (LSP) は、補完候補や定義箇所といったソースコードに対する情報を提供するツール(Language Server)と、それを用いるテキストエディタやIDEとのやりとりを標準化したプロトコルです。このプロトコルが普及する以前では、「各言語毎に専用に作られたプラグインをエディタに導入する」というのが一般的でしたが、現在では「エディタには単一のLSPクライントを導入し、LSP 経由で各言語の Language Server と連携する」というのが一般的になりつつあります。 筆者は LSP 経由で補完候補の表示、定義箇所へのジャンプ、シンボルの一覧表示、シンボルのリネーム、コンパイルエラーや Linter の違反情報の表示などを行っています。LSP クライアントとしての Vim プラグインはいくつか存在しますが、下記の3つが代表的です。

筆者はこの3つのプラグインを全て導入して定期的に試していますが、fzf との連携が組み込まれているという観点から、現在は LanguageClient-neovim を主に利用しています。下記は筆者が行っている LanguageClient-neovim の設定の一部です(設定の内容についてはLanguageClient-neovimのドキュメントを参照してください)。

" 各言語の Language Server の起動コマンド
let g:LanguageClient_serverCommands = {
            ...
            \    'go': [
            \      '/path/to/gopls',
            \      '-remote=:37374',
            \      '-remote.logfile=/tmp/gopls.remote.log',
            \ ]
            \    'rust': [
            \      '/path/to/rls',
            \    ],
            ...
            \ }
let g:LanguageClient_diagnosticsSignsMax                = v:null
let g:LanguageClient_changeThrottle                     = v:null
let g:LanguageClient_autoStart                          = 1
let g:LanguageClient_autoStop                           = 1
let g:LanguageClient_selectionUI                        = 'fzf'
let g:LanguageClient_selectionUI_autoOpen               = 0
let g:LanguageClient_trace                              = 'off'
let g:LanguageClient_diagnosticsEnable                  = 1
let g:LanguageClient_loadSettings                       = 1
let g:LanguageClient_windowLogMessageLevel              = 'Warning'
let g:LanguageClient_hoverPreview                       = 'Always'
let g:LanguageClient_fzfContextMenu                     = 1
let g:LanguageClient_diagnosticsList                    = 'Quickfix'
let g:LanguageClient_useVirtualText                     = 'Diagnostics'
let g:LanguageClient_virtualTextPrefix                  = '>> '
let g:LanguageClient_useFloatingHover                   = 1
let g:LanguageClient_usePopupHover                      = 1
let g:LanguageClient_floatingHoverHighlight             = 'Normal:Pmenu'
let g:LanguageClient_completionPreferTextEdit           = 0
let g:LanguageClient_waitOutputTimeout                  = 100
let g:LanguageClient_echoProjectRoot                    = 0
let g:LanguageClient_hideVirtualTextsOnInsert           = 1
let g:LanguageClient_diagnosticsMaxSeverity             = 'Hint'
let g:LanguageClient_applyCompletionAdditionalTextEdits = 1
let g:LanguageClient_preferredMarkupKind                = ['markdown', 'plaintext']
let g:LanguageClient_floatingWindowStyle                = 'minimal'
let g:LanguageClient_diagnosticsIgnoreSources           = ['go mod tidy']

" ドキュメントのハイライトをトグルさせるための設定
let s:ls_is_highlighting_document = v:false
function! s:ls_toggle_document_highlight() abort
    if !s:ls_is_highlighting_document
        call LanguageClient#textDocument_documentHighlight()
        let s:ls_is_highlighting_document = v:true
    else
        call LanguageClient#clearDocumentHighlight()
        let s:ls_is_highlighting_document = v:false
    end
endfunction

" ドキュメントの表示をトグルさせるための設定
function! s:ls_toggle_document_hover() abort
    let hover_buf = s:ls_get_hover_buf()
    if hover_buf == 0
        call LanguageClient#textDocument_hover()
    else
        execute(printf('bwipe! %s', hover_buf))
    endif
endfunction

function! s:ls_get_hover_buf() abort
    for w in nvim_list_wins()
        let bufnum = nvim_win_get_buf(w)
        let bufname = bufname(bufnum)
        if bufname ==# '__LanguageClient__'
            return bufnum
        endif
    endfor

    return 0
endfunction

" 定義ジャンプ後にコールバックで呼ぶ関数
function! Language_client_definition_callback(output, ...) abort
    normal! zz
    call vista#util#Blink(3, 100)
endfunction

" マッピングの設定
function! s:map_language_client_functions() abort
    if has_key(g:LanguageClient_serverCommands, &filetype)
        nnoremap <silent> gv        :<C-u>call LanguageClient#explainErrorAtPoint()<CR>
        nnoremap <silent> gi        :<C-u>call LanguageClient#textDocument_implementation()<CR>
        nnoremap <silent> gkr       :<C-u>call LanguageClient#textDocument_references()<CR>
        nnoremap <silent> gr        :<C-u>call LanguageClient#textDocument_rename()<CR>
        nnoremap <silent> <Leader>i :<C-u>call <SID>ls_toggle_document_hover()<CR>
        nnoremap <silent> gd        :<C-u>call LanguageClient#textDocument_definition({'handle': v:true}, function('Language_client_definition_callback'))<CR>
        nnoremap <silent> gsi       :<C-u>silent call <SID>ls_toggle_document_highlight()<CR>
        nnoremap <silent> gc        :<C-u>silent Vista finder fzf:lcn<CR>
    endif
endfunction

autocmd vimrc FileType * call s:map_language_client_functions()

LanguageClient-neovim は補完のためのプラグインである deoplete.nvim と連携する機能を内包しており、 deoplete.nvim が導入されていると自動的に deoplete.nvim 経由で補完候補が表示されます。下図は LanguageClient-neovimdeoplete.nvim を連携させて補完を行っている例です。

補完を行っている例

関数のシグネチャは echodoc.vim というプラグインを用いてカーソルの上の別ウィンドウ(Floating Window)に表示しています。

Git

筆者は tig を利用して Git の各操作を行っています。tig は CLI で動作する Git クライアントであり、 git add などの操作を CLI でインタラクティブに行うためのツールです。この tig を Neovim のターミナル機能を用いて Vim 上で起動しています。下図は Vim 上で tig を起動し、対象のファイルを git add した後に git commit する例です。

Vim上のターミナルからtigを起動する例

上図で、Vim 上のターミナルから起動した tiggit commit を行うときに、新たな Vim のプロセスを生成するのではなく、親側の Vim プロセスでコミットの編集画面が開く点に注目してください。これは neovim-remote という Neovim をリモートで操作するためのツールを用いて実現しています。 これを踏まえて、上図の操作は下記のような設定を用いて実行されています。

" neovim-remote
let nvrcmd      = "nvr --remote-wait -cc 'call NvrBeforeCmd()' -c 'call NvrAfterCmd()'"
let $VISUAL     = nvrcmd
let $GIT_EDITOR = nvrcmd

nnoremap <silent> <Leader>t :<C-u>silent call <SID>tig_status()<CR>

function! s:tig_status() abort
    call s:open_term('tig status')
endfunction

function! s:open_term(cmd) abort
    let split = s:split_type()

    call execute(printf('%s term://%s', split, a:cmd))

    setlocal bufhidden=delete
    setlocal noswapfile
    setlocal nobuflisted
endfunction

function! s:split_type() abort
    " NOTE: my cell ratio: width:height == 1:2.1
    let width = winwidth(win_getid())
    let height = winheight(win_getid()) * 2.1

    if height > width
        return 'split'
    else
        return 'vsplit'
    endif
endfunction

Vim の起動時に VISUALGIT_EDITOR 環境変数を nvr (neovim-remote が提供するコマンド)に置き換えることで、Vim 上から起動した tig でコミットを行うときは nvr --remote-wait ... を経由して親側の Neovim プロセスでコミット編集画面が開くように調整しています。 s:open_term 関数はターミナルでプログラムを開くときのバッファの設定を調整するための関数です。s:split_type 関数は新規ウィンドウを縦・横どちらの分割で開くかを決定するためのもので、これを用いて「現在のウィンドウが縦長なときは横分割、横長なときは縦分割」で新規ウィンドウを開くようにしています。

ファイラ

ファイルの作成や削除、コピー&ペーストは defx.nvim というファイラプラグインを用いて下図のように Vim 上から実行できるようにしています。

Vim上から新規ファイルを作成する例

上図の操作は下記のような defx.nvim の設定を行うことで実現しています。

autocmd vimrc FileType defx call s:map_defx_functions()
autocmd vimrc FileType defx call defx#custom#column('mark', {
            \   'length':        1,
            \   'readonly_icon': 'X',
            \   'selected_icon': '*',
            \ })
autocmd vimrc FileType defx call defx#custom#column('indent', {
            \   'indent': '  ',
            \ })
autocmd vimrc FileType defx call defx#custom#column('icon', {
            \   'directory_icon': '▸',
            \   'opened_icon':    '▾',
            \   'root_icon':      '  ',
            \ })

autocmd vimrc BufLeave,BufWinLeave \[defx\]* silent call defx#call_action('add_session')

function! s:map_defx_functions() abort
    nnoremap <silent><buffer><expr> <CR>      <SID>defx_edit()
    nnoremap <silent><buffer><expr> c         defx#do_action('copy')
    nnoremap <silent><buffer><expr> m         defx#do_action('move')
    nnoremap <silent><buffer><expr> p         defx#do_action('paste')
    nnoremap <silent><buffer><expr> l         defx#do_action('open')
    nnoremap <silent><buffer><expr> o         defx#do_action('open_or_close_tree')
    nnoremap <silent><buffer><expr> O         defx#do_action('open_tree_recursive')
    nnoremap <silent><buffer><expr> E         defx#do_action('open', 'vsplit')
    nnoremap <silent><buffer><expr> P         defx#do_action('open', 'pedit')
    nnoremap <silent><buffer><expr> K         defx#do_action('new_directory')
    nnoremap <silent><buffer><expr> N         defx#do_action('new_file')
    nnoremap <silent><buffer><expr> d         defx#do_action('remove')
    nnoremap <silent><buffer><expr> r         defx#do_action('rename')
    nnoremap <silent><buffer><expr> x         defx#do_action('execute_system')
    nnoremap <silent><buffer><expr> yy        defx#do_action('yank_path')
    nnoremap <silent><buffer><expr> .         defx#do_action('toggle_ignored_files')
    nnoremap <silent><buffer><expr> h         defx#do_action('cd', ['..'])
    nnoremap <silent><buffer><expr> ~         defx#do_action('cd')
    nnoremap <silent><buffer><expr> q         defx#do_action('quit')
    nnoremap <silent><buffer><expr> <Leader>q defx#do_action('quit')
    nnoremap <silent><buffer><expr> <Space>   defx#do_action('toggle_select') . 'j'
    nnoremap <silent><buffer><expr> *         defx#do_action('toggle_select_all')
    nnoremap <silent><buffer><expr> <C-l>     defx#do_action('redraw')
    nnoremap <silent><buffer><expr> <C-g>     defx#do_action('print')
    nnoremap <silent><buffer><expr> cd        defx#do_action('change_vim_cwd')
    nnoremap <silent><buffer><expr> j         line('.') == line('$') ? 'gg' : 'j'
    nnoremap <silent><buffer><expr> k         line('.') == 1 ? 'G' : 'k'

    nnoremap <silent><buffer> D :<C-u>call <SID>defx_reload()<CR>
endfunction

nnoremap <silent> <Leader>e :<C-u>silent call <SID>open_or_close_defx()<CR>

function! s:defx_edit(...) abort
    let args = defx#util#convert2list(get(a:000, 0, []))
    let cmd = printf(":\<C-u>call DefxEditFile(%s)\<CR>", string(args))
    return cmd
endfunction

function! DefxEditFile(args, ...) abort
    " ...
    call defx#call_action('open', a:args)
endfunction

function! s:open_or_close_defx() abort
    if &filetype ==# 'defx'
        bwipe
    else
        call s:open_defx()
    end
endfunction

function! s:open_defx() abort
    let opts = [
                \   '-no-show-ignored-files',
                \   '-ignored-files=.git,.nvimrc,.lc.*,_tmp',
                \   '-sort=filename',
                \   '-no-listed',
                \   '-no-new',
                \   '-buffer-name=defx',
                \   '-split=no',
                \   printf('-session-file=%s', '.defx_session.json'),
                \ ]

    call defx#util#call_defx('Defx', join(opts, ' '))

    if len(filter(getbufinfo({'buflisted': 1}), 'v:val.name !=# ""')) == 0
        call s:wipe_all_listed_buffers()
    endif
endfunction

function! s:defx_reload() abort
    execute 'normal q'
    call s:defx_delete_session()
    call s:open_defx()
endfunction

function! s:defx_delete_session() abort
    let jq_expression = printf(
                \   '.sessions | map_values(select(.path != "%s")) | {version: "1.0", sessions: .}',
                \   getcwd(),
                \ )
    let jq_cmd = printf("jq '%s'", jq_expression)
    let cmd = printf('J=$(cat %s | %s) && echo $J > %s', '.defx_session.json', jq_cmd, '.defx_session.json')

    call system(cmd)
endfunction

スニペット

タイプ数を極力減らすために筆者はコードスニペットを多用しています。スニペット用のプラグインとしては UltiSnips を利用しています。 UltiSnips は下記のように設定しています。

let g:UltiSnipsUsePythonVersion    = 3
let g:UltiSnipsEditSplit           = 'normal'
let g:UltiSnipsSnippetDirectories  = ['/path/to/snippets_dir']
let g:UltiSnipsEnableSnipMate      = 0
let g:UltiSnipsExpandTrigger       = '<c-k>'
let g:UltiSnipsJumpForwardTrigger  = '<c-f>'
let g:UltiSnipsJumpBackwardTrigger = '<c-b>'
command! SNIP UltiSnipsEdit

設定にある通り、基本的にはスニペットを入力した後に ctrl + k を入力することでスニペットを展開していますが、 UltiSnips の自動展開機能を用いて、特定の文字列が入力されたときはトリガー(ctrl + k)の入力なしで自動でスニペットを展開するように設定しています。下図はその例です。

自動展開スニペットの例

上図では、 cbac という入力が行われたときにスニペットのトリガーである ctrl + k の入力を行わずに context.Background() が展開されています。このスニペットは下記のような設定になっています。

snippet cbac "context.Background" wA
context.Background()
endsnippet

UltiSnips ではスニペットに A オプションを渡すことでトリガーを入力せずにスニペットを自動展開させることができます。 UltiSnips はこの他にも Python や Vim Script を用いてスニペットの展開内容をダイナミックに生成する機能などを持っており、これを用いることで柔軟にスニペットを生成することが可能となります。

Goの設定

gopls

Go の Language Server としては公式に提供されている gopls を利用しています。gopls はエディタを起動したときに gopls 自体も同時に起動するのが一般的ですが、この方法だと gopls の起動を待つ時間が発生しやすくなります。これをできるだけ回避するために、筆者は goplsdaemon mode で起動しています。daemon modegopls を起動すると gopls は daemon プロセスとして常に動き続け、エディタは起動時にその daemon プロセスに接続する形となります。これにより gopls の起動待ちの時間を短縮することができます。筆者は主に Linux(Ubuntu)で開発しているので、下記のような設定で systemd を用いて gopls プロセスを永続化しています。

[Unit]
Description=Gopls Remote Instance

[Service]
Type=simple
ExecStart=/bin/bash -c 'PATH=$HOME/go/bin:$PATH /path/to/gopls -listen=:37374 -logfile=/tmp/gopls.daemon.log'
Restart=always

[Install]
WantedBy=default.target

テストの実行

実装中にテストを頻繁に実行するので、下図のようにカーソル上のテスト関数を go test -run で Vim 上から実行できるように設定しています。

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

これを行うために、自作した go-test-name というツールを用いて下記のような設定を行っています。

autocmd vimrc FileType go nnoremap <silent> gt :<C-u>silent call <SID>go_test_function()<CR>

function! s:go_test_function() abort
    let test_info = json_decode(system(printf('go-test-name -pos %s -file %s', s:cursor_byte_offset(), @%)))

    for b in nvim_list_bufs()
        if bufname(b) ==# 'vim-go-test-func'
            execute printf('bwipe! %s', b)
        endif
    endfor

    let dir = expand('%:p:h')

    if len(test_info.sub_test_names) > 0
        let cmd = printf("go test -coverprofile='/tmp/go-coverage.out' -count=1 -v -race -run='^%s$'/'^%s$' $(go list %s)", test_info.test_func_name, test_info.sub_test_names[0], dir)
    else
        let cmd = printf("go test -coverprofile='/tmp/go-coverage.out' -count=1 -v -race -run='^%s$' $(go list %s)", test_info.test_func_name, dir)
    endif

    let split = s:split_type()
    execut printf('%s gotest', split)

    if split ==# 'split'
        execute(printf('resize %s', floor(&lines * 0.3)))
    endif

    call termopen(cmd)
    setlocal bufhidden=delete
    setlocal noswapfile
    setlocal nobuflisted
    file vim-go-test-func
    wincmd p
endfunction

function! s:cursor_byte_offset() abort
    return line2byte(line('.')) + (col('.') - 2)
endfunction

go-test-name はソースコードを静的に解析し、カーソル(バイトオフセット)上のテスト関数名とサブテスト名をJSONで返すツールです。このツールが返す情報を、 go test コマンドの -run 引数に渡し、Vim 上のターミナルから実行することにより、単体のテスト、及びサブテストのみをエディタから即座に実行できるように設定しています。

デバッグの実行

筆者はテストの実行をデバッガ経由で実行することで、プログラムのデバッグを行っています。Go で書いたプログラムのデバッグには delve を用いてます。テストと同様にカーソル上のテスト関数を dlv test で Vim 上から実行できるように設定しています。

Vim上でdelveを起動してデバッグする例

上図は、テスト関数内にブレークポイントを設定した後にデバッガを実行し、変数の中身を print して動作の確認を行っている例です。設定は下記のようになっています。

command! -nargs=* -bang GoDebugTestFunc call s:go_debug_test_function(<bang>0, 0, <f-args>)
autocmd vimrc FileType go nnoremap <silent> gb  :<C-u>GoDebugTestFunc<CR>
autocmd vimrc Filetype go command! -nargs=* -bang BP call s:go_debug_toggle_break_point(<f-args>)
autocmd vimrc Filetype go command! -nargs=* -bang BPC call s:go_debug_clear_breakpoionts()

function! s:go_debug_test_function(bang, ...) abort
    call writefile(g:godebug_breakpoints + ['continue'], g:godebug_breakpoints_file)

    let test_info = json_decode(system(printf('go-test-name -pos %s -file %s', s:cursor_byte_offset(), @%)))
    let test = search(printf('func %s', test_info.test_func_name), 'bcnW')

    let tmpl = "cd %s && GOMAXPROCS=1 dlv test --init=%s -- -test.run='^%s$'"
    let wd = expand('%:h')

    let dlv = printf(tmpl, wd, g:godebug_breakpoints_file, test_info.test_func_name)

    if len(test_info.sub_test_names) > 0
        let dlv = printf("%s/'^%s$'", dlv, test_info.sub_test_names[0])
    endif

    enew
    set syntax=go
    call termopen(dlv)
    file debug
    start
endfunction

function! s:go_debug_toggle_break_point(...) abort
    let bp_file = expand('%:p')

    let line = line('.')
    let breakpoint = printf('break %s:%s', bp_file, line)

    exe 'sign define gobreakpoint text=>> texthl=EmphasisLightBlue'

    let i = index(g:godebug_breakpoints, breakpoint)
    if i == -1
        call add(g:godebug_breakpoints, breakpoint)
        execute printf('sign place %s line=%s name=gobreakpoint file=%s', line, line, bp_file)
    else
        call remove(g:godebug_breakpoints, i)
        execute printf('sign unplace %s file=%s', line, bp_file)
    endif
endfunction

function! s:go_debug_clear_breakpoionts() abort
    for b in g:godebug_breakpoints
        let point = matchlist(b, '\vbreak (.+):(\d+)')
        let file = point[1]

        execute printf('sign unplace %s file=%s', point[2], file)
    endfor
    let g:godebug_breakpoints = []
endfunction

function! s:go_debug_delete_break_points_file(...) abort
    if filereadable(g:godebug_breakpoints_file)
        call delete(g:godebug_breakpoints_file)
    endif
endfunction

「テスト」の項で解説したのと同様に、 go-test-name を用いてカーソル上のテスト関数名を特定し、そのテスト関数名を dlv test コマンドの -test.run 引数に渡して Vim 上のターミナルで実行するように設定しています。(この設定は vim-godebug というプラグインを参考にしています。)

interface の実装

impl という Go の interface のスタブを生成するツールを用いて、カーソル上の struct に対して プロジェクト内の interface を実装させる設定を行っています。

interfaceのスタブを生成する例

上図の例は下記のような impl 連携の設定を用いて実現しています。

autocmd vimrc Filetype go command! IMP call s:go_fzf_implement_interface()

function! s:go_fzf_implement_interface() abort
    let source = 'go_list_interfaces'

    call fzf#run({
                \   'source': source,
                \   'sink':   function('s:go_implement_interface'),
                \   'down':   '40%'
                \ })
endfunction

function! s:go_implement_interface(interface) abort
    call s:go_execute_impl(a:interface, v:false)
endfunction

function! s:go_execute_impl(interface, is_std_pkg) abort
    let pos = getpos('.')
    let recv = s:go_receiver()

    " Make sure we put the generated code *after* the struct.
    if getline('.') =~# 'struct '
        normal! $%
    endif

    if !a:is_std_pkg
        let pkg = system('go mod edit -json | jq -r .Module.Path | tr -d "\n"')
        if a:interface =~# '^\.'
            let interface = printf('%s%s', pkg, a:interface)
        else
            let interface = printf('%s/%s', pkg, a:interface)
        endif
    else
        let interface = a:interface
    end

    try
        let dirname = fnameescape(expand('%:p:h'))
        let [result, err] = go#util#Exec(['impl', '-dir', dirname, recv, interface])
        let result = substitute(result, "\n*$", '', '')
        if err
            call go#util#EchoError(result)
            return
        endif

        if result is# ''
            return
        end

        put =''
        silent put =result
    finally
        call setpos('.', pos)
    endtry
endfunction

上記の設定の一部は Vim 用の Go プラグインである vim-go から拝借しています。上記の設定で呼び出している go_list_interfaces コマンドは inteface の一覧を表示するためのもので、下記のようなシェルスクリプトになっています。

#!/bin/bash

rg '^type [A-Z].* interface' --type go --ignore-file ~/.ignore --no-heading --no-line-number | while read line; do
    pkg=$(echo -e $line | perl -nle 'm/^(.*)\/(.+)\.go:/; print $1')

    type=$(echo -e $line | perl -nle 'm/^.*type (.*) interface/; print $1')

    echo "${pkg}.${type}"
done

無理やりシェルスクリプトで interface を絞り込んでいますが、本来は静的解析で行うべきものなので、knife などのツールを利用するように修正しようと考えています。

式の補完

今回のイベントのために録画した自分の開発動画を見返すことにより、筆者は「右辺を入力してから左辺を書く」ことが多いということに気づきました。そこから発想を得て新たに go-expr-completion というツールを開発しました。このツールはソースコードを静的解析して「カーソル上の式についての型情報」を返すツールです。これを用いてカーソル上の式に対する左辺を自動生成するための Vim プラグインが vim-go-expr-completion です。下図はこのプラグインを用いて左辺を自動生成している例です。

左辺を自動生成する例

vim-go-expr-completion は式に対する左辺と同時に、必要であればエラーチェックのための if err != nil 文も自動で生成します。

おわりに

本稿では、筆者が Vim(Neovim)の環境をどのように構築しているかについて解説しました。Go は静的解析用のライブラリを標準で提供しており、エディタ用のツールが比較的簡単に実装できるので、皆様もぜひ新しい便利ツールを作ってエディタを拡張してみてはいかがでしょうか。本稿で紹介した内容が皆様のエディタライフになにか寄与できれば光栄です。それでは。