콘텐츠로 건너뛰기

[Vim] Neovim을 C++ IDE로 사용하기

  • by
vim as IDE(??)

오랜만에 C++로 코드를 작성해야 할 일이 생겼다. 훑어봐야 할 코드가 좀 방대해서 vscode의 편리한 gui를 이용해볼까 하다가, 아무래도 마우스 없이 tmux 환경에서 window, pane을 여러 개로 나눠서 vim, gdb를 오가는 것에 너무 익숙해져 있어서 계속 vim으로 코드를 작성하기로 했다. 그냥 적당히 syntax highlighting 해주는 plugin에 ctag를 사용해도 사실 문제는 없지만, overloaded 함수를 찾아가는 것이나 필요할 때 바로 데이터 타입 확인, 자동완성 등 IDE의 intellisense가 그리울 때가 종종 있다. 찾아보니 LSP (language server protocol)을 사용하는 세련된 방법들이 보여서 그에 맞춰서 vim 세팅을 새로 정리했다.

새로 정리한 vim 세팅은 먼저 정리하자면 다음과 같다
Neovim + vim-plug + coc.nvim (+coc-clangd) + vim-lsp-cxx-highlight
설치하다 보면 어차피 관련 doc를 읽어야 하지만, 한국인이라면 아무래도 한글로 된 짧은 문서를 선호하기 마련이니 설치, 세팅 방법을 아래 정리한다.

Neovim

Neovim의 최신 stable 버전은 글을 작성하는 시점에서 0.4.4이다. 사실 꼭 Neovim이 필요한 것은 아니고, Vim도 버전 8 이상이면 된다. 다만 이 글에서는 Neovim 기준으로 설명하니, Vim을 사용할 예정이라면 각 프로그램의 홈페이지에서 설치 방법을 자세히 읽어봐야 한다. 플러그인 설치 방법이나 설정 script 등에서 조금씩 다른 부분이 있다.

sudo apt install neovim 으로 설치하거나, 또는 github release 페이지에서 appimage를 다운받아 실행하면 된다.나는 appimage를 다운받은 후 nvim으로 링크를 걸어줬다.

ln -s nvim.appimage ~/bin/nvim

또한 vim을 완전히 대체해서 사용할 예정이기에 ~/.bashrc에 아래 내용을 입력해 vim 명령어도 nvim으로 alias 걸어두었다.

export PATH=${HOME}/bin:${PATH}
alias vim='nvim'
alias vimdiff='nvim -d'

Neovim의 설정 파일은 ~/.config/nvim/init.vim이다. 처음에는 없으니 직접 만들어주거나, vim 실행 후 :help nvim-from-vim의 1.번 항목을 따라하면 된다.

:call mkdir(stdpath('config'), 'p'))
:exe 'edit '.stdpath('config').'/init.vim'

vim-plug

vim-plug은 Vim의 plugin manager이다. Neovim의 경우 아래 명령어로 설치한다.

sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

설치 후 init.vim의 최상단에 아래 내용을 작성한다.

" Specify a directory for plugins
call plug#begin()

" Make sure you use single quotes
" Plug 'neoclide/coc.nvim', {'branch': 'release'}
" Plug 'jackguo380/vim-lsp-cxx-highlight'
" Plug 'vim-airline/vim-airline'
" Plug 'vim-airline/vim-airline-themes'
" Plug 'scrooloose/nerdtree'


" Initialize plugin system
call plug#end()

call plug#begin()call plug#end() 사이에 Plug 명령어로 사용할 플러그인을 명시한다. 자세한 사용 방법은 홈페이지 참고. 눈치 챘겠지만 위의 코드에서 주석처리 되어있는 5개 플러그인이 현재 내가 사용중인 플러그인이다.

coc.nvim + coc-clangd

coc.nvim은 vim에 LSP 지원을 추가해주는 플러그인이다. 그 외에 json을 통한 VSCode와 유사한 설정 방법, 수많은 extension 등을 장점으로 내세우는 듯 하다. 간단히 둘러보니 여러 언어에 대해 extension이 준비되어 있어서 대부분의 언어는 coc.nvim + 해당 언어 extension으로 커버될 것 같다.

설치에 10.12버전 이상의 Node.js가 필요하다. curl -sL install-node.now.sh/lts | bash 명령어를 통해 설치하거나 홈페이지에서 binary 파일을 직접 다운로드 받으면 된다.

설치 후 반드시 Example vim configuration 섹션을 확인하고 init.vim 파일에 적절히 반영해주자. 편리한 키매핑 및 코딩시 도움이 되는 설정들 (e.g. statusline에 현재 함수 표시)이 포함되어 있다.

coc.nvim은 LSP의 client에 해당하므로, 사용할 언어의 language server가 추가로 필요하다. 나는 language server로 clangd를 사용할 것이므로 coc-clangd를 설치했다.

:CocInstall coc-clangd

clangd를 사용하므로 물론 시스템에 clangd가 설치되어 있어야 한다. 아래 명령어를 통해 coc.nvim에서도 직접 설치할 수 있다고 하니 참고.

:CocCommand clangd.install

clangd가 정상 동작 하려면 C++ 프로젝트에서 compile_commands.json 파일을 만들어줘야 한다. clangd의 project setup 페이지를 참고해 만들어주자. CMake를 사용한다면 CMakeLists.txt 파일에 아래 문구를 추가해주면 된다.

set ( CMAKE_EXPORT_COMPILE_COMMANDS on )

vim-lsp-cxx-highlight

vim-lsp-cxx-highlight는 LSP를 이용해 C, C++, Cuda, ObjC 코드에 semantic highlighting을 해주는 플러그인이다. :CocConfig를 통해 확인할 수 있는 coc-settings.json 파일에 아래와 같이 입력해 줘야 한다. 두 번째 line은 위에서 언급했던 coc.nvim의 현재 함수 확인 기능 관련한 코드이니 필요 없다면 빼도 된다.

{
    "clangd.semanticHighlighting": true,
    "coc.preferences.currentFunctionSymbolAutoUpdate": true
}

추가 설정들

vim-airline + coc_current_function

vim 기본 테마에서의 coc_current_function
vim-airline custom integration

coc.nvim에의 Example vim configuration을 보면 layout이 맘에 안들긴 하지만 vim의 status line에 현재 함수를 표시해주는 기능이 있다. 그런데 vim-airline 플러그인을 사용하면 해당 기능이 작동하지 않는다. 이는 vim-airline과 coc.nvim의 integration이 애초에 coc_status()만을 받아오게 되어 있기 때문이다. ~/.config/nvim/plugged/vim-airline/autoload/airline/extensions/coc.vim의 coc_status(), init() 함수를 살펴보면 알 수 있다.

statusline의 section C에 현재 함수를 표시하도록 하기 위해 아래 코드를 init.vim에 추가했다.

" Custom vim-airline integration
function! StatusDiagnostic() abort
	let info = get(b:, 'coc_diagnostic_info', {})
	if empty(info) | return '' | endif
	let msgs = []
	if get(info, 'error', 0)
		call add(msgs, 'E' . info['error'])
	endif
	if get(info, 'warning', 0)
		call add(msgs, 'W' . info['warning'])
	endif
	return join(msgs, ' '). ' ' . get(g:, 'coc_status', '') . ' ' . get(b:, 'coc_current_function', '')
endfunction

let g:airline_section_c = '%{StatusDiagnostic()}'

signcolumn + number column

signcolumn in Neovim

Neovim에 coc.vim을 처음 설치하면 위와 같이 못생긴 signcolumn이 하나 생긴다. 최신의 vim 버전에서는 signcolumn과 number column의 merge를 지원하기 때문에 신경 쓸 필요가 없지만, 안타깝게도 stable release의 Neovim은 지원하지 않는 기능이며, 다음 releas에서의 지원이 예정되어 있다.

Signcolunm을 끄고 대신에 표시할 사항이 있다면 number column의 숫자를 수정하도록 아래 코드를 init.vim에 추가했다.

" Settings for neovim: nevoim does not support signcolumn+number column yet
set signcolumn=no
hi coc_err_hi ctermfg=1 ctermbg=15
sign define coc_err numhl=coc_err_hi
sign place 1 line=2 name=coc_err

Tmux 사용시 Esc 키 delay

설치 후 tmux에서 vim을 사용하다 보면 esc키를 눌렀을 때 vim이 살짝 프리징 되는듯한 느낌을 받을 수 있다. 이는 tmux에서 Esc 키의 키 바인딩을 위해 약간의 대기 시간이 지정되어 있기 때문이다. ~/.tmux.conf에 아래 코드를 추가해 Esc 키에 대한 딜레이를 없앨 수 있다.

set -sg escape-time 0

ColorScheme 관련

vimdiff before/after changing highlight colors

나는 vim의 colorscheme으로 desert를 주로 사용한다. 별다른 이유는 없고, 처음에 vim을 접할 때 여러 colorscheme 중에서 desert의 색이 맘에 들어 계속 사용해왔고, 그래서인지 지금 와서는 다른 colorscheme들의 색이 너무 어색해 적응하기 힘들다.

그런데 desert의 기본 색은 vimdiff, coc.nvim의 floating window 등에서 배경색 및 글자색이 모두 비슷한 빨간색으로 보여서 가독석이 영 좋지 않다. init.vim에 아래 코드를 추가해 색을 변경했다.

colo desert
highlight Normal     ctermbg=NONE
highlight DiffAdd    cterm=bold ctermfg=10 ctermbg=17 gui=none guifg=bg guibg=Red
highlight DiffDelete cterm=bold ctermfg=10 ctermbg=17 gui=none guifg=bg guibg=Red
highlight DiffChange cterm=bold ctermfg=10 ctermbg=17 gui=none guifg=bg guibg=Red
highlight DiffText   cterm=bold ctermfg=10 ctermbg=88 gui=none guifg=bg guibg=Red

" Floating window background=white, foreground=black
" CocFloating - Pmenu의 옆에 나오는 box & error시 나오는 box
highlight CocFloating ctermbg=white ctermfg=black

현재는 gruvbox colorscheme을 사용하는 중이다. 따라서 위의 코드 블록은 필요 없다. 처음에는 좀 어색했는데 익숙해지니 더 가시성도 좋고 편한 것 같다. 사실 이 글의 이미지들은 모두 desert로 되어 있는데… 따로 업데이트 하지는 않음.

사용중인 init.vim 설정

내가 현재 사용중인 init.vim 파일을 아래에 공유한다. coc.nvim에서 가져온 코드가 많아서 공유 할 만큼 빼어난 설정이 있는건 아니지만 그래도 혹시 이 글을 읽는 분이 있다면 도움이 되었으면 한다.

" NeoVIM Config File ... linked to ~/.config/nvim/init.vim
" Specify a directory for plugins
" - For Neovim: stdpath('data') . '/plugged'
" - Avoid using standard Vim directory names like 'plugin'
call plug#begin()

" Make sure you use single quotes
Plug 'neoclide/coc.nvim', {'branch': 'release'}
Plug 'vim-airline/vim-airline'
Plug 'vim-airline/vim-airline-themes'
Plug 'scrooloose/nerdtree'
" cxx highlight features are attractive, but make vim slow
Plug 'jackguo380/vim-lsp-cxx-highlight'
Plug 'morhetz/gruvbox'
Plug 'chrisbra/csv.vim'

" Initialize plugin system
call plug#end()

"====================================================================================================
" coc.nvim
" TextEdit might fail if hidden is not set.
set hidden

" Some servers have issues with backup files, see #649.
set nobackup
set nowritebackup

" Give more space for displaying messages.
set cmdheight=2

" Having longer updatetime (default is 4000 ms = 4 s) leads to noticeable
" delays and poor user experience.
set updatetime=300

" Don't pass messages to |ins-completion-menu|.
set shortmess+=c

" Always show the signcolumn, otherwise it would shift the text each time
" diagnostics appear/become resolved.
if has("patch-8.1.1564")
  " Recently vim can merge signcolumn and number column into one
  set signcolumn=number
else
  set signcolumn=yes
endif

" Settings for neovim: nevoim does not support signcolumn+number column yet
set signcolumn=no
hi coc_err_hi ctermfg=1 ctermbg=15
sign define coc_err numhl=coc_err_hi
sign place 1 line=2 name=coc_err

hi CocErrorSign ctermfg=gray

" Use tab for trigger completion with characters ahead and navigate.
" NOTE: Use command ':verbose imap <tab>' to make sure tab is not mapped by
" other plugin before putting this into your config.
inoremap <silent><expr> <TAB>
      \ pumvisible() ? "\<C-n>" :
      \ <SID>check_back_space() ? "\<TAB>" :
      \ coc#refresh()
inoremap <expr><S-TAB> pumvisible() ? "\<C-p>" : "\<C-h>"

function! s:check_back_space() abort
  let col = col('.') - 1
  return !col || getline('.')[col - 1]  =~# '\s'
endfunction

" Use <c-space> to trigger completion.
if has('nvim')
  inoremap <silent><expr> <c-space> coc#refresh()
else
  inoremap <silent><expr> <[email protected]> coc#refresh()
endif

" Make <CR> auto-select the first completion item and notify coc.nvim to
" format on enter, <cr> could be remapped by other vim plugin
inoremap <silent><expr> <cr> pumvisible() ? coc#_select_confirm()
                              \: "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"

" Use `[g` and `]g` to navigate diagnostics
" Use `:CocDiagnostics` to get all diagnostics of current buffer in location list.
nmap <silent> [g <Plug>(coc-diagnostic-prev)
nmap <silent> ]g <Plug>(coc-diagnostic-next)

" GoTo code navigation.
nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gi <Plug>(coc-implementation)
nmap <silent> gr <Plug>(coc-references)

" Use K to show documentation in preview window.
nnoremap <silent> K :call <SID>show_documentation()<CR>

function! s:show_documentation()
  if (index(['vim','help'], &filetype) >= 0)
    execute 'h '.expand('<cword>')
  elseif (coc#rpc#ready())
    call CocActionAsync('doHover')
  else
    execute '!' . &keywordprg . " " . expand('<cword>')
  endif
endfunction

" Highlight the symbol and its references when holding the cursor.
autocmd CursorHold * silent call CocActionAsync('highlight')

" Symbol renaming.
nmap <leader>rn <Plug>(coc-rename)

" Formatting selected code.
xmap <leader>f  <Plug>(coc-format-selected)
nmap <leader>f  <Plug>(coc-format-selected)

augroup mygroup
  autocmd!
  " Setup formatexpr specified filetype(s).
  autocmd FileType typescript,json setl formatexpr=CocAction('formatSelected')
  " Update signature help on jump placeholder.
  autocmd User CocJumpPlaceholder call CocActionAsync('showSignatureHelp')
augroup end

" Applying codeAction to the selected region.
" Example: `<leader>aap` for current paragraph
xmap <leader>a  <Plug>(coc-codeaction-selected)
nmap <leader>a  <Plug>(coc-codeaction-selected)

" Remap keys for applying codeAction to the current buffer.
nmap <leader>ac  <Plug>(coc-codeaction)
" Apply AutoFix to problem on the current line.
nmap <leader>qf  <Plug>(coc-fix-current)

" Map function and class text objects
" NOTE: Requires 'textDocument.documentSymbol' support from the language server.
xmap if <Plug>(coc-funcobj-i)
omap if <Plug>(coc-funcobj-i)
xmap af <Plug>(coc-funcobj-a)
omap af <Plug>(coc-funcobj-a)
xmap ic <Plug>(coc-classobj-i)
omap ic <Plug>(coc-classobj-i)
xmap ac <Plug>(coc-classobj-a)
omap ac <Plug>(coc-classobj-a)

"" Remap <C-f> and <C-b> for scroll float windows/popups.
"if has('nvim-0.4.0') || has('patch-8.2.0750')
"  nnoremap <silent><nowait><expr> <C-f> coc#float#has_scroll() ? coc#float#scroll(1) : "\<C-f>"
"  nnoremap <silent><nowait><expr> <C-b> coc#float#has_scroll() ? coc#float#scroll(0) : "\<C-b>"
"  inoremap <silent><nowait><expr> <C-f> coc#float#has_scroll() ? "\<c-r>=coc#float#scroll(1)\<cr>" : "\<Right>"
"  inoremap <silent><nowait><expr> <C-b> coc#float#has_scroll() ? "\<c-r>=coc#float#scroll(0)\<cr>" : "\<Left>"
"  vnoremap <silent><nowait><expr> <C-f> coc#float#has_scroll() ? coc#float#scroll(1) : "\<C-f>"
"  vnoremap <silent><nowait><expr> <C-b> coc#float#has_scroll() ? coc#float#scroll(0) : "\<C-b>"
"endif

" Use CTRL-S for selections ranges.
" Requires 'textDocument/selectionRange' support of language server.
nmap <silent> <C-s> <Plug>(coc-range-select)
xmap <silent> <C-s> <Plug>(coc-range-select)

" Add `:Format` command to format current buffer.
command! -nargs=0 Format :call CocAction('format')

" Add `:Fold` command to fold current buffer.
command! -nargs=? Fold :call     CocAction('fold', <f-args>)

" Add `:OR` command for organize imports of the current buffer.
command! -nargs=0 OR   :call     CocAction('runCommand', 'editor.action.organizeImport')

" integrate vim-airline  (:h airline-coc) -> Does not work as I expected!
" let g:airline#extensions#coc#enabled = 1
" let g:airline#extensions#coc#error_symbol = 'E:'
" let g:airline#extensions#coc#warning_symbol = 'W:'
" let g:airline#extensions#coc#stl_format_err = '%E{[%e(#%fe)]}'
" let g:airline#extensions#coc#stl_format_warn = '%W{[%e(#%fw)]}'

" Add (Neo)Vim's native statusline support. -> Does not work as I expected!
" NOTE: Please see `:h coc-status` for integrations with external plugins that
" provide custom statusline: lightline.vim, vim-airline.
"set statusline^=%{coc#status()}%{get(b:,'coc_current_function','')}

" Custom vim-airline integration
function! StatusDiagnostic() abort
	let info = get(b:, 'coc_diagnostic_info', {})
	if empty(info) | return '' | endif
	let msgs = []
	if get(info, 'error', 0)
		call add(msgs, 'E' . info['error'])
	endif
	if get(info, 'warning', 0)
		call add(msgs, 'W' . info['warning'])
	endif
	return join(msgs, ' '). ' ' . get(g:, 'coc_status', '') . ' ' . get(b:, 'coc_current_function', '')
endfunction

let airline_section_c = '%{StatusDiagnostic()}'
"airline_section_b is empty now, because corresponding plugins (e.g. coc-git) are not installed
" but maybe not required

" Mappings for CoCList
" Show all diagnostics.
nnoremap <silent><nowait> <Leader>ca  :<C-u>CocList diagnostics<cr>
" Manage extensions.
nnoremap <silent><nowait> <Leader>ce  :<C-u>CocList extensions<cr>
" Show commands.
nnoremap <silent><nowait> <Leader>cc  :<C-u>CocList commands<cr>
" Find symbol of current document.
nnoremap <silent><nowait> <Leader>co  :<C-u>CocList outline<cr>
" Search workspace symbols.
nnoremap <silent><nowait> <Leader>cs  :<C-u>CocList -I symbols<cr>
" Do default action for next item.
nnoremap <silent><nowait> <Leader>cj  :<C-u>CocNext<CR>
" Do default action for previous item.
nnoremap <silent><nowait> <Leader>ck  :<C-u>CocPrev<CR>
" Resume latest coc list.
nnoremap <silent><nowait> <Leader>cp  :<C-u>CocListResume<CR>
"====================================================================================================
" nerdtree
map <Leader>nt <ESC>:NERDTree<CR>

" vim-airline
" Powerline-font 활성화
let g:airline_powerline_fonts = 1
let g:airline#extensions#tabline#enabled = 1 " turn on buffer list
let g:airline_theme='luna'
" Tab line 에 파일명만 출력되도록 설정
let g:airline#extensions#tabline#formatter = 'unique_tail'
set laststatus=2 " turn on bottom bar

" NERDTree ON 단축키를 "\nt"로 설정
map <Leader>nt <ESC>:NERDTree<CR>

"====================================================================================================
set termguicolors
set background=dark
let g:gruvbox_italic=1
let g:gruvbox_contrast_dark='hard'
colo gruvbox
"colo desert
"highlight Normal     ctermbg=NONE
"highlight DiffAdd    cterm=bold ctermfg=10 ctermbg=17 gui=none guifg=bg guibg=Red
"highlight DiffDelete cterm=bold ctermfg=10 ctermbg=17 gui=none guifg=bg guibg=Red
"highlight DiffChange cterm=bold ctermfg=10 ctermbg=17 gui=none guifg=bg guibg=Red
"highlight DiffText   cterm=bold ctermfg=10 ctermbg=88 gui=none guifg=bg guibg=Red

" Floating window background=white, foreground=black
" CocFloating - Pmenu의 옆에 나오는 box & error시 나오는 box
"highlight CocFloating ctermbg=white ctermfg=black

"syntax
"syntax on

"tap size
set tabstop=4
set shiftwidth=4
set expandtab

"line number
set number
"set smartindent

"hilight search
set hlsearch
nnoremap <silent> <Space> :nohlsearch<Bar>:echo<CR> 

"backspace
set backspace=indent,eol,start
set t_kb=

"mouse wheel
set mouse=a

"set nowrap
set wrap

" reduce empty space at bottom
set cmdheight=1

map <F10> :echo "hi<" . synIDattr(synID(line("."),col("."),1),"name") . '> trans<'
\ . synIDattr(synID(line("."),col("."),0),"name") . "> lo<"
\ . synIDattr(synIDtrans(synID(line("."),col("."),1)),"name") . ">"<CR>

"====================================================================================================
"avoid overriding variables py Vim filetype
autocmd FileType python setlocal tabstop=4
Share this post!

답글 남기기

이메일 주소는 공개되지 않습니다.

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.