こちらは AWS CDK Advent Calendar 2021 の 10 日目の記事です。
こんにちは、高野(@konokenj)です。AWS CDK v2 がリリースされましたね!
VSCode で AWS CDK を開発するガイドはいくつかありますが、今回は私の好きな Vim で開発するためのプラグインと設定を紹介します。
後半では、Amazon EC2 インスタンス上に構築した Ubuntu 20.04 の環境でゼロから Vim を設定し、AWS CDK を記述してデプロイする手順を紹介します。
vim-lsp を中心として TypeScript の開発に必要な最小限の Vim プラグインを組み込んだ vimrc の全文を掲載しています。この設定を適用すると、次のようなイメージで編集が可能になります。
import 文を書かなくても自動的にモジュールが追加されている様子が分かるでしょうか。ここにもシングルパッケージ化された AWS CDK v2 のメリットが現れていますね。
入力中の構文チェック結果や入力補完、リファクタリング機能として識別子のリネームと定義元へのジャンプも試しています。TypeScript の LSP だけでなく、ESLint と Prettier も連携されています。
Vim の設定には AWS CDK 固有の設定は必要ありません。TypeScript での開発において汎用的に使える内容となっています。Vim は使い慣れているけれど VSCode はあまり使い込んでいない、という方が AWS CDK や TypeScript の開発にチャレンジするきっかけになるとうれしいです。
Vimrc とプラグインの解説
バッファを編集する操作は主に <leader>
キーを使って設定しています。<leader>
は <space>
にしていますが、お好みで変更してください。
let g:mapleader = "\<Space>"
vim-plug
Vim プラグインの管理には vim-plug を使用しています。シンプルな設定で外部依存がなく、非同期読み込みにも対応しているためです。
vim-plug 自体の読み込み処理は vimrc には設定されていません。インストールするには curl
を使用して Vim script を ~/.vim/autoload/
配下に Vim スクリプトをダウンロードします。このパスにあるスクリプトは Vim の起動時に自動で読み込まれます。詳細は :h autoload
を参照してください。
vim-lsp
プログラミング言語のオートコンプリート(補完)、定義箇所へのジャンプ、関数のシグネチャやドキュメントの表示、コンパイルエラー情報の表示などの機能は、Language Server Protocol(LSP) によって提供されるのが一般的になってきました。
テキストエディタのプラグインが LSP とやりとりするインタフェースを実装していれば、ユーザーは使いたいプログラミング言語の LSP サーバを導入するだけでよいわけです。 Vim では、LSP のサポートに vim-lsp を使用します。
vim-lsp はインストールしただけではキーマッピングを定義しないため、自分で任意の設定を行う必要があります。また、LSP が有効になっているバッファだけでキーマッピングを定義するために autocmd
を使用して関数を呼び出しています。
" vim-lsp がバッファで有効になったときに実行される関数
" バッファローカルなキーバインドやオプションを指定
" See: https://mattn.kaoriya.net/software/vim/20191231213507.htm
function! s:on_lsp_buffer_enabled() abort
if &ft =~ 'ctrlp\|dirvish'
return
endif
setlocal omnifunc=lsp#complete
setlocal signcolumn=yes
nmap <buffer> <leader>a <plug>(lsp-code-action)
nmap <buffer> <leader>l <plug>(lsp-code-lens)
nmap <buffer> <leader>L <plug>(lsp-document-diagnostics)
" nmap <buffer> <leader>d <plug>(lsp-decralation)
nmap <buffer> <leader>D <plug>(lsp-definition)
nmap <buffer> <leader>y <plug>(lsp-type-definition)
nmap <buffer> <leader>i <plug>(lsp-implementation)
nmap <buffer> <leader>r <plug>(lsp-references)
nmap <buffer> <leader>R <plug>(lsp-rename)
nmap <buffer> <leader>S <plug>(lsp-document-symbol)
nmap <silent><buffer> <C-j> <plug>(lsp-next-diagnostic)
nmap <silent><buffer> <C-k> <plug>(lsp-previous-diagnostic)
if &ft =~ 'typescript\|javascript'
nmap <buffer> <leader>f :LspDocumentFormatSync --server=efm-langserver<CR>
xmap <buffer> <leader>f :LspDocumentRangeFormatSync --server=efm-langserver<CR>
else
nmap <buffer> <leader>f <plug>(lsp-document-format)
xmap <buffer> <leader>f <plug>(lsp-document-range-format)
endif
endfunction
augroup lsp_install
au!
autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
" vim-lsp デバッグログの出力
command! LspDebug let lsp_log_verbose=1 | let lsp_log_file = expand('~/lsp.log')
" lightline.vim のステータスラインに vim-lsp diagnostics の数を表示する
" augroup LightLineOnLSP
" autocmd!
" autocmd User lsp_diagnostics_updated call lightline#update()
" augroup END
let g:lsp_diagnostics_echo_cursor = 1
Plug 'prabirshrestha/vim-lsp'
vim-lsp-settings
vim-lsp に各プログラミング言語の LSP サーバを設定するには多くの設定が必要です。これをシンプルにするため、 vim-lsp-settings 使用します。たとえば TypeScript ファイルを開いたら typescript-language-server
のインストールをサジェストし、:LspInstallServer
コマンドを実行するとこれを自動的にインストールして vim-lsp を設定してくれます。
vim-lsp-settings の設定方法は 2 つあります。vimrc 内で g:lsp_settings
を使用する方法と、:LspSettingsGlobalEdit
コマンド経由で JSON ファイルを編集する方法(通常は ~/.local/share/vim-lsp-settings/settings.json
に保存)です。今回は前者を採用しています。
Plug 'mattn/vim-lsp-settings'
efm-langserver
ESLint や Prettier など、LSP に準拠しない開発ツールも存在します。これらのツールを LSP のインタフェースに合わせて呼び出せるようにするのが efm-langserver です。efm-langserver は設定次第で任意のツールを非同期に呼び出すことができます。これによって ALE や Syntastic を必要とせず、vim-lsp だけでシンプルに完結できます。余談ですが、efm って何?と思った方は :h efm
を参照してください。
efm-langserver は Golang で実装されたサーバ本体と Vim プラグインがセットになっています。まず、サーバをインストールします。
# go v1.16 以上の場合
go install github.com/mattn/efm-langserver@latest
efm-langserver の設定ファイルは通常 ~/.config/efm-langserver/config.yaml
が使用されます。このファイルを次の内容で作成します。TypeScript 以外のツールの設定を行うには、GitHub の README に書かれているサンプルを参考にしてください。私はこの記事の執筆にも、efm-langserver 経由で textlint を使用しています。
log-file のパスは適宜修正してください。
version: 2
root-markers:
- .git/
lint-debounce: 1s
log-file: /home/ubuntu/efm-langserver.log # FIXME
log-level: 1
tools:
javascript-eslint: &javascript-eslint
lint-command: './node_modules/.bin/eslint -f visualstudio --stdin --stdin-filename ${INPUT}'
lint-ignore-exit-code: true
lint-stdin: true
lint-formats:
- "%f(%l,%c): %tarning %m"
- "%f(%l,%c): %rror %m"
typescript-prettier: &typescript-prettier
format-command: './node_modules/.bin/prettier ${--tab-width:tabWidth} ${--single-quote:singleQuote} --parser typescript --stdin-filepath ${INPUT}'
format-stdin: true
languages:
typescript:
- <<: *javascript-eslint
- <<: *typescript-prettier
vimrc には、vim-lsp-settings を使って efm-langserver を認識させる設定を入れます。
また、TypeScript ファイルの保存前に自動的に Prettier を呼び出すための autocmd
も定義しています。
" vim-lsp 経由でバッファの保存前にフォーマットをかける
" See: https://zenn.dev/yami_beta/articles/589c567199ea28
augroup MyLSPTypeScript
autocmd!
autocmd BufWritePre *.ts,*.tsx call execute('LspDocumentFormatSync --server=efm-langserver')
augroup END
let g:lsp_settings = {
\ 'efm-langserver': {
\ 'disabled': v:false
\ },
\ }
Plug 'mattn/efm-langserver'
asyncomplete.vim
補完(オートコンプリート)には vim-lsp と同じ作者で相性の良い asyncomplete を使用します。このプラグインは依存関係として async.vim が必要であることと、vim-lsp と連携させるために asyncomplete-lsp.vim も一緒にインストールします。
let g:asyncomplete_popup_delay = 200
Plug 'prabirshrestha/async.vim'
Plug 'prabirshrestha/asyncomplete.vim'
Plug 'prabirshrestha/asyncomplete-lsp.vim'
vim-vsnip
スニペットには vim-vsnip を使用します。VSCode のスニペットと互換性がり、Vim script のみで実装されているシンプルなプラグインです。これと vim-lsp を連携させるために vim-vsnip-integ と、スニペットライブラリの friendly-snippets もを合わせてインストールします。
vim-vsnip もデフォルトのキーマッピングを持ちません。ヘルプ :h vim-vsnip
の例をそのまま使用しました。補完のポップアップが表示されているときに候補を確定してすぐに改行したいケースがあるため、<CR>
のマッピングを追加しています。
let g:vsnip_snippet_dir = expand($XDG_CONFIG_HOME . '/vsnip')
" 補完のポップアップメニュー表示中
" <CR> で候補を確定
inoremap <expr><CR> pumvisible() ? "\<c-y>" : "\<cr>"
" :h vsnip-mapping
" Expand
imap <expr> <C-j> vsnip#expandable() ? '<Plug>(vsnip-expand)' : '<C-j>'
smap <expr> <C-j> vsnip#expandable() ? '<Plug>(vsnip-expand)' : '<C-j>'
" Expand or jump
imap <expr> <C-l> vsnip#available(1) ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>'
smap <expr> <C-l> vsnip#available(1) ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>'
" Jump forward or backward
imap <expr> <Tab> vsnip#jumpable(1) ? '<Plug>(vsnip-jump-next)' : '<Tab>'
smap <expr> <Tab> vsnip#jumpable(1) ? '<Plug>(vsnip-jump-next)' : '<Tab>'
imap <expr> <S-Tab> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-Tab>'
smap <expr> <S-Tab> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-Tab>'
Plug 'hrsh7th/vim-vsnip'
Plug 'hrsh7th/vim-vsnip-integ'
Plug 'rafamadriz/friendly-snippets'
editoconfig-vim
EditorConfig は、プロジェクト内でコードのフォーマット(インデントスタイルや改行コードなど)を統一するためのアプリケーションです。チーム開発でのトラブルを避けるため、Vim に限らずすべてのエディタで有効にしておくとよいでしょう。余談ですが、以前は Vim に +python
が必要でしたが今は Vim script で実装された core がプラグインに含まれるようになったため外部依存がなくなり、Vim script のみで動作します。
Plug 'editorconfig/editorconfig-vim'
vim-commentary
コメントアウト、アンコメントを行うプラグインです。gc
などデフォルトのキーマッピングを持ちますので、:h commentary
を参照してください。
Plug 'tpope/vim-commentary'
lexima.vim
VSCode などでよくある、閉じ括弧良い感じに入力してくれるプラグインです。
Plug 'cohama/lexima.vim'
完成形
全体の vimrc は以下の通りになります。
scriptencoding utf-8
let g:mapleader = "\<Space>"
call plug#begin('~/.local/share/vim/bundle')
" vim-lsp がバッファで有効になったときに実行される関数
" バッファローカルなキーバインドやオプションを指定
" See: https://mattn.kaoriya.net/software/vim/20191231213507.htm
function! s:on_lsp_buffer_enabled() abort
if &ft =~ 'ctrlp\|dirvish'
return
endif
setlocal omnifunc=lsp#complete
setlocal signcolumn=yes
nmap <buffer> <leader>a <plug>(lsp-code-action)
nmap <buffer> <leader>l <plug>(lsp-code-lens)
nmap <buffer> <leader>L <plug>(lsp-document-diagnostics)
" nmap <buffer> <leader>d <plug>(lsp-decralation)
nmap <buffer> <leader>D <plug>(lsp-definition)
nmap <buffer> <leader>y <plug>(lsp-type-definition)
nmap <buffer> <leader>i <plug>(lsp-implementation)
nmap <buffer> <leader>r <plug>(lsp-references)
nmap <buffer> <leader>R <plug>(lsp-rename)
nmap <buffer> <leader>S <plug>(lsp-document-symbol)
nmap <silent><buffer> <C-j> <plug>(lsp-next-diagnostic)
nmap <silent><buffer> <C-k> <plug>(lsp-previous-diagnostic)
if &ft =~ 'typescript\|javascript'
nmap <buffer> <leader>f :LspDocumentFormatSync --server=efm-langserver<CR>
xmap <buffer> <leader>f :LspDocumentRangeFormatSync --server=efm-langserver<CR>
else
nmap <buffer> <leader>f <plug>(lsp-document-format)
xmap <buffer> <leader>f <plug>(lsp-document-range-format)
endif
endfunction
augroup lsp_install
au!
autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
" vim-lsp デバッグログの出力
command! LspDebug let lsp_log_verbose=1 | let lsp_log_file = expand('~/lsp.log')
" lightline.vim のステータスラインに vim-lsp diagnostics の数を表示する
" augroup LightLineOnLSP
" autocmd!
" autocmd User lsp_diagnostics_updated call lightline#update()
" augroup END
" vim-lsp 経由でバッファの保存前にフォーマットをかける
" See: https://zenn.dev/yami_beta/articles/589c567199ea28
augroup MyLSPTypeScript
autocmd!
autocmd BufWritePre *.ts,*.tsx call execute('LspDocumentFormatSync --server=efm-langserver')
augroup END
" asyncomplete.vim
let g:asyncomplete_popup_delay = 200
" vim-lsp
let g:lsp_diagnostics_echo_cursor = 1
let g:lsp_settings = {
\ 'efm-langserver': {
\ 'disabled': v:false
\ },
\ }
" vim-vsnip
let g:vsnip_snippet_dir = expand($XDG_CONFIG_HOME . '/vsnip')
" 補完のポップアップメニュー表示中
" <CR> で候補を確定
inoremap <expr><CR> pumvisible() ? "\<c-y>" : "\<cr>"
" :h vsnip-mapping
" Expand
imap <expr> <C-j> vsnip#expandable() ? '<Plug>(vsnip-expand)' : '<C-j>'
smap <expr> <C-j> vsnip#expandable() ? '<Plug>(vsnip-expand)' : '<C-j>'
" Expand or jump
imap <expr> <C-l> vsnip#available(1) ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>'
smap <expr> <C-l> vsnip#available(1) ? '<Plug>(vsnip-expand-or-jump)' : '<C-l>'
" Jump forward or backward
imap <expr> <Tab> vsnip#jumpable(1) ? '<Plug>(vsnip-jump-next)' : '<Tab>'
smap <expr> <Tab> vsnip#jumpable(1) ? '<Plug>(vsnip-jump-next)' : '<Tab>'
imap <expr> <S-Tab> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-Tab>'
smap <expr> <S-Tab> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-Tab>'
Plug 'prabirshrestha/async.vim'
Plug 'prabirshrestha/asyncomplete.vim'
Plug 'prabirshrestha/asyncomplete-lsp.vim'
Plug 'prabirshrestha/vim-lsp'
Plug 'mattn/vim-lsp-settings'
Plug 'mattn/efm-langserver'
Plug 'hrsh7th/vim-vsnip'
Plug 'hrsh7th/vim-vsnip-integ'
Plug 'rafamadriz/friendly-snippets'
" editorconfig-vim
" .editorconfig ファイルを参照してインデントや改行コードなどをプロジェクト内で統一
Plug 'editorconfig/editorconfig-vim'
" vim-commentary
" `gc` でコメント、アンコメント
Plug 'tpope/vim-commentary'
" lexima.vim
" 閉じカッコを良い感じに補完
Plug 'cohama/lexima.vim'
call plug#end()
Amazon EC2 で Vim を設定して AWS CDK をデプロイしてみる
vimrc の動作検証のため、新しい Amazon EC2 インスタンスで Vim をインストールして AWS CDK を実装し、デプロイしてみます。手元の環境で実行する場合はパッケージのインストールなどの不要な部分を適宜読み飛ばしてください。
今回は Ubuntu 20.04 がインストールされた Amazon EC2 c5.large インスタンスを用意しました。
Vim と Golang のインストール
Golang は efm-langserver のインストールのために使用しました。なお、efm-langserver はバイナリ版も提供されているため、GitHub Release から直接インストールする場合 Golang のインストールは不要です。
$ sudo apt-get update
$ sudo apt-get install vim golang-go unzip -y
$ go version
go version go1.13.8 linux/amd64
$ vim --version
VIM - Vi IMproved 8.1 (2018 May 18, compiled Nov 08 2021 14:21:34)
(省略)
AWS CLI v2 のインストールと設定
こちらを参照してください。aws configure
で適宜クレデンシャルを設定します。
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
aws --version
aws configure
vim-plug のインストール
最新の手順は GitHub を確認してください。
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
efm-langserver のインストール
~/.profile に GOPATH
を定義して、パスを通します。
export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin
efm-langserver をインストールします。Ubuntu 20.04 では Golang のバージョンが少し古いため go get
を使用しましたが、 Go v1.16 以上の場合は go install
を使用してください。
# go v1.16 未満の場合
go get github.com/mattn/efm-langserver
# go v1.16 以上の場合
go install github.com/mattn/efm-langserver@latest
次に設定ファイルを作成します。設定ファイルの内容は こちら からコピー&ペーストしてください。
mkdir -p ~/.config/efm-langserver
touch ~/.config/efm-langserver/config.yaml
vim ~/.config/efm-langserver/config.yaml
設定ファイルを作成したら、動作確認しておきます。エラーなく起動できることを確認したら、Ctrl-C
で終了します。
efm-langserver
Node.js のインストール
nvm を使用して LTS 版の Node.js をインストールします。合わせて npm もアップデートしておきます。
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
. ~/.nvm/nvm.sh
nvm ls-remote --lts
nvm install --lts=gallium
nvm use --lts=gallium
npm i -g npm
node --version
npm --version
AWS CDK プロジェクトの作成
こちらの手順を参考に hello world してみます。
mkdir hello-cdk
cd hello-cdk
git init .
git config user.name hoge
git config user.email hoge@example.com
git commit --allow-empty -m "initial commit"
npx -p aws-cdk cdk init --language typescript
git add -A
git commit -m "cdk init"
npm run build
npx cdk ls
ESLint, Prettier, EditorConfig の導入
ESLint と Prettier をインストールします。
npm i -D \
@typescript-eslint/eslint-plugin \
@typescript-eslint/parser \
eslint \
eslint-config-prettier \
prettier
.eslintrc.json ファイルを以下の内容で作成します。
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}
.editorconfig ファイルを以下の内容で作成します。なお、前述の通り editorconfig-vim は EditorConfig Core を含むためプロジェクト単位で npm i editorconfig
を実行する必要はありません。
root = true
# base settings
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# by extensions
[*.sh]
end_of_line = lf
[*.bat]
end_of_line = crlf
[*.md]
trim_trailing_whitespace = false
indent_size = 4
[*.{yml,yaml}]
indent_size = 4
[{Makefile,go.mod,go.sum,*.go}]
indent_style = tab
indent_size = 4
Vim で編集
vimrc を配置します。内容はこちらをコピー&ペーストしてください。
mkdir -p ~/.vim
vim ~/.vim/vimrc
Vim をいったん終了し、再起動します。
vim
プラグインをインストールするために :PlugInstall
を実行して、成功したら Vim を一度終了します。
:PlugInstall
Vim で TypeScript ファイルを編集してみます。
vim lib/hello-cdk-stack.ts
:LspInstallServer
を実行するようにサジェストされるため、これに従います。
:LspInstallServer
あとは思う存分コードを編集していきましょう。記事の冒頭で紹介した Gif アニメによるデモのような編集が可能になっています。参考までに、作成していたコードは以下です。
import { Stack, StackProps, aws_s3, aws_iam } from "aws-cdk-lib";
import { Construct } from "constructs";
export class HelloCdkStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const myBucket = new s3.Bucket(this, "MyBucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
const myLambdaRole = new iam.Role(this, 'MyLambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
})
myBucket.grantRead(myLambdaRole);
}
}
LSP は非同期に起動されているため、Vim を起動した直後は補完やコードアクションが実行されない場合があります。通常は Vim を立ち上げたまま複数のファイルを編集するためあまり問題にはなりませんが、今回のデモのように 1 ファイルをさっと編集してすぐに Vim を終了するようなケースでは、補完が動き始めるまで数秒待機する必要があることもあります。
数秒待っても補完がうまくいかない場合は :LspStatus を実行して LSP サーバの実行状態を確認してください。また、:LspDebug を実行することで ~/lsp.log にログが出力されます。
ESLint は実行に時間がかかるため、typescript-language-server よりも遅れて結果が表示されます。
AWS CDK をデプロイ
npx cdk bootstrap
npx cdk synth
npx cdk deploy
終わりに
Vim でも VSCode などのモダンなエディタと遜色なく開発支援機能を活用できます。ここで紹介している通り、書かなければならない vimrc のコード量も非常に少なくなっています。Vim には慣れていても、なんとなくモダンなエディタや言語に壁を感じている方は、ぜひこの機会に試してみてください。Happy Coding!