본문으로 건너뛰기

neovim의 발을 들이면서...

· 약 42분
arch-spatula

오늘은 발렌타인 데이입니다. 저는 여자에게 받는 초코렛은 못 받아도 스스로 IDE는 선물로 줄 수 있을 것같습니다.

neovim을 설정하는 여정입니다. 이제 일상적으로 c 언어를 사용해야 할 때 사용할만할 정도로 개발환경이 좋아졌습니다.

warning

이글은 대부분의 개발자에게 가치가 없는 글입니다. 아주 소수의 개발자에게만 가치가 있습니다.

저는 맥북 에어 M1 유저로서 neovim을 설정하는 과정입니다. 이글은 저의 neovim 여행의 시작이고 이 시작을 여러분에게 알려주기 위해 쓰는 글입니다.

neovim 시작

배경은 C 언어를 공부하는 중에 VSCode를 그만 사용하고 싶어졌습니다. 하나는 기본 플러그인 설치로 개발하는 프로젝트를 만들 때마다 자꾸 파일을 만듭니다. 저는 이 부분이 싫었습니다.

C 언어를 올바르게 다루려면 에디터부터 올바르게 다루는 방법부터 학습해야 할 것 같았습니다. MS가 제공하는 비주얼 스튜디오도 싫었습니다. CLion도 있지만 이것은 유료입니다. 이때 정말 많은 사람들이 사용하는데 저도 같이 neovim을 사용해보고자 했습니다.

vim motion 자체는 알고 있습니다. 하지만 VS Code에 사용하면 플러그인이 충돌하는 것인지 오류가 많아졌습니다. 그래서 어느정도 익숙해졌는데 다시 해제했니다. 나중에는 nvchad를 발견하고 조금 사용했습니다. 하지만 그렇게 사용하면 결국 남이 만들어 주는 것이라 뭔가 어색했습니다. 분명 좋은 neovim distro는 맞지만 저는 제가 설정할 neovim을 온전히 이해하고 싶었습니다. 특별히 도움 되지 않지만 도움된다고 착각하면서 보는 테크 유튜버kickstart.nvim를 추천해줬고 distro를 잠시 옮겼습니다. 이 distro는 아니지만 꽤 좋았습니다. 그리고 내부 소스코드를 보니까 비교적 단순했고 저의 두뇌 성능으로 이해할 수 있었습니다. 최소한 이해하고 있다는 착각을 하고 있었습니다.

vim은 이펙트티브하려면 숙련도가 필요합니다. 본인이 설정하기 때문에 본인이 에디터에 대해서 자각하는 부분이 많아집니다. 그리고 성능이 꽤 좋다고 착각하면서 사용하는 에디터입니다. 저는 직업은 개발자인 주제에 홍대병 말기라 저만의 간지나는 에디터를 사용하고 싶었습니다. 해피해킹 키보드에 스타벅스에서 neovim으로 판교에서 코딩하고 있으면 아무도 신경 안쓰는데 간지날 것 같습니다.

VSCode를 사용하다 보면 아쉬운 점이 많습니다. 글을 쓰는 중에 이동하려면 커맨드 팔레트(cmd + p > @)를 사용해 접근하기 때문입니다. 이동을 하고 글이 하단에 있으면 상단 혹은 중간으로 끌어 올리고 싶은데 단축키를 모릅니다.

일개 1년 정도 프로그래밍 공부하고 회사 수습 기간 끝난지 얼마 안 된 나약한 현대 웹개발자가 vim을 논하기는 웃기지만 그래도 블로그 소재를 위해 논하겠습니다. 참고로 vim에 대한 내용은 블로그 vim 문서에 중복해서 작성 될 것입니다. 문서는 정리하고 찾기 쉬워야합니다. 하지만 이 글은 저의 여정에 관한 글입니다.

neovim 설치

neovim을 설치하고 그냥 사용하는 것은 가능할 것입니다. 하지만 제가 2년 뒤 노트북 바꿀 때 제가 제 블로그 보고 했던 설정 또하기 위해서 작성해두겠습니다.

먼저 zsh이 설치 되어 있는지 확인해봅니다. 일반적으로 맥북은 기본설치가 되어 있을 것입니다.

zsh --version

만약에 설치되어 있다면 다음은 neovim을 설치하겠습니다.

brew install neovim

설치 가이드를 보고 알아서 설치하기 바랍니다. 또 brew도 없으면 일상생활을 어떻게 하는지 모르겠지만 설치하기 바랍니다.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # brew 있으면 생략

home brew 홈페이지에서 가져왔습니다.

이제 oh-my-zsh을 설치하겠습니다. omz는 vi 커맨드를 사용해도 nvim으로 커맨드를 에일리어싱하기 위해서 필요합니다.

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" # omz 있으면 생략
cd # 루트 폴더로 이동
vi .zshrc

위 파일에서 다음처럼 수정하기 바랍니다.

.zshrc
alias vim="nvim"
alias vi="nvim"
alias vimdiff="nvim -d"
export EDITOR=/usr/local/bin/nvim # M1이면 /opt/homebrew/bin/nvim

마지막은 neovim을 실행 전 설정입니다. 이렇게 하면 기존 vi를 실행하지 않고 새롭게 설치한 neovim을 사용하게 됩니다. 마치 백엔드 nginx 백워드 프록시처럼 기존 vi로 향할 커맨드가 새로운 nvim으로 향하도록 합니다.

보존

git clone 본인원격레포 ~/.config/nvim --depth 1 && nvim

우리가 원하는 것은 git clone 만으로 개발하는 환경을 바꿔도 neovim 환경을 일관되게 사용하고 싶습니다.

cd ~/.config/nvim
vi init.lua

먼저 init.lua를 설정하기 바랍니다. 여기서는 파일만 만들고 끝내도 됩니다.

init.lua
print("hello neovim")

:wq 다음 명령을하고 저장하고 닫기바랍니다.

git init
git add init.lua
git commit -m "neovim init"
# 그래봤자 github이겠지만 알아서 원격 연결하기 바랍니다.
git push

여기서 팁을 하나 주자면 본인 개인 레포를 만들기 바랍니다. 본인 레포에서 본인의 개발환경도 이슈로 관리하기 바랍니다. 또 github-flow 방식으로 레포를 운영하면 효율적일 것입니다.

삭제

# Linux / Macos (unix)
rm -rf ~/.config/nvim
rm -rf ~/.local/share/nvim

가끔은 다른 distro를 경험하고 영감을 받고 싶을 때가 있습니다. 잠시 삭제하고 본인 것을 나중에 다시 설치하기 바랍니다.

또 마음에 안든다 싶으면 그냥 삭제하고 처음부터 다시 만들 때도 은근히 있습니다. 뭐가 뭐하는 녀석인지 잘 모르기 시작하고 어디를 점진적으로 분리하기 시작했는데 오히려 문제가 많아지면 합니다.

플러그인 매니져

플러그인 매니져를 사용해도 괜찮고 안 사용해도 괜찮습니다. 저는 지금은 저의 편의성을 위해 일단 플러그인 매니저를 사용하겠습니다.

플러그인 매니저는 크게 2가지가 있습니다.

어느것을 선택하는 사실 크게 상관없습니다. 본인 기계의 neovim 버전이 맞으면 됩니다. 저는 lazy.nvim을 선택했습니다. kickstart에서 사용하고 있었기 때문입니다. 여기서 저는 양심의 가책도 없이 일부를 복붙했습니다.

init.lua
-- lazyvim 플러그인 설치
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)

require('lazy').setup({})

여기까지 했다면 플러그인 매니저를 설치한 것입니다. 다음은 테마 설정입니다.

테마 설정

편집하기 시작할 때 가장 의욕을 많이 주는 것은 테마일 것입니다. 저에게는 그랬습니다.

init.lua
-- lazyvim 플러그인 설치
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)

require('lazy').setup({
{ "catppuccin/nvim", name = "catppuccin", priority = 1000 },
})

위처럼 설정하기 바랍니다. 그리고 껐다가 키면 뭔가가 설치될 것입니다. 또 껐다키면 완전히 설정될 것입니다. 여기서부터 에디터가 간지나기 시작할 것입니다.

여기서 정말로 중요한 것은 공식 레포를 들어가보는 활동입니다. 저는 catppuccin을 설치하고자 했습니다. neovim이 어느 레포에서 가져오는지 무슨 이름으로 설정되는지 알아내는 것이 중요합니다.

lua/plugins/theme.lua
return {
"catppuccin/nvim",
name = "catppuccin",
priority = 1000,
init = function()
-- catppuccin 컬러 태마 설정
vim.cmd.colorscheme "catppuccin-mocha"
end
}

init은 load 전에 실행할 수 있게 해줍니다.

init.lua
require('lazy').setup({
require 'plugins.theme',
})

위처럼 관심사를 분리하는 것도 가능합니다. 어느정도 설정이 커지기 시작하면 관심사를 분리하는 것이 좋습니다. 중요한 것은 neovim을 실행할 때 init.lua가 실행된다는 점입니다. 그래서 가져오게 만들고 실행하도록 해야 합니다.

git 설정

neovim github 설정 후 이미지

git 설정을 하면 위처럼 될 것입니다.

저는 위 설정을 kickstart에서 복붙했습니다. 그래서 사실 무슨 내용인지 자세히는 모릅니다.

lua/plugins/git.lua
return {
-- git 설정
-- Adds git related signs to the gutter, as well as utilities for managing changes
'lewis6991/gitsigns.nvim',
opts = {
-- See `:help gitsigns.txt`
signs = {
add = { text = '+' },
change = { text = '~' },
delete = { text = '_' },
topdelete = { text = '‾' },
changedelete = { text = '~' },
},
on_attach = function(bufnr)
local gs = package.loaded.gitsigns

local function map(mode, l, r, opts)
opts = opts or {}
opts.buffer = bufnr
vim.keymap.set(mode, l, r, opts)
end

-- Navigation
map({ 'n', 'v' }, ']c', function()
if vim.wo.diff then
return ']c'
end
vim.schedule(function()
gs.next_hunk()
end)
return '<Ignore>'
end, { expr = true, desc = 'Jump to next hunk' })

map({ 'n', 'v' }, '[c', function()
if vim.wo.diff then
return '[c'
end
vim.schedule(function()
gs.prev_hunk()
end)
return '<Ignore>'
end, { expr = true, desc = 'Jump to previous hunk' })

-- Actions
-- visual mode
map('v', '<leader>hs', function()
gs.stage_hunk { vim.fn.line '.', vim.fn.line 'v' }
end, { desc = 'stage git hunk' })
map('v', '<leader>hr', function()
gs.reset_hunk { vim.fn.line '.', vim.fn.line 'v' }
end, { desc = 'reset git hunk' })
-- normal mode
map('n', '<leader>hs', gs.stage_hunk, { desc = 'git stage hunk' })
map('n', '<leader>hr', gs.reset_hunk, { desc = 'git reset hunk' })
map('n', '<leader>hS', gs.stage_buffer, { desc = 'git Stage buffer' })
map('n', '<leader>hu', gs.undo_stage_hunk, { desc = 'undo stage hunk' })
map('n', '<leader>hR', gs.reset_buffer, { desc = 'git Reset buffer' })
map('n', '<leader>hp', gs.preview_hunk, { desc = 'preview git hunk' })
map('n', '<leader>hb', function()
gs.blame_line { full = false }
end, { desc = 'git blame line' })
map('n', '<leader>hd', gs.diffthis, { desc = 'git diff against index' })
map('n', '<leader>hD', function()
gs.diffthis '~'
end, { desc = 'git diff against last commit' })

-- Toggles
map('n', '<leader>tb', gs.toggle_current_line_blame, { desc = 'toggle git blame line' })
map('n', '<leader>td', gs.toggle_deleted, { desc = 'toggle git show deleted' })

-- Text object
map({ 'o', 'x' }, 'ih', ':<C-U>Gitsigns select_hunk<CR>', { desc = 'select git hunk' })
end,
},
}
init.lua
require('lazy').setup({
require 'plugins.theme',
require 'plugins.gitPlugin' -- 추가
})

fuzzy finder(검색기)

저는 이번에 telescope.nvim을 설정해보겠습니다. vim에서 가장 유명한 fuzzy finder입니다.

telescope

lua/plugins/telescope.lua
return {
'nvim-telescope/telescope.nvim',
branch = '0.1.x',
dependencies = {
'nvim-lua/plenary.nvim',
{
'nvim-telescope/telescope-fzf-native.nvim',
-- NOTE: If you are having trouble with this installation,
-- refer to the README for telescope-fzf-native for more instructions.
build = 'make',
cond = function()
return vim.fn.executable 'make' == 1
end,
},
},
init = function()
-- 검색기 활성화
local builtin = require('telescope.builtin')
vim.keymap.set('n', '<leader>ff', builtin.find_files, {}) -- 파일이름으로 검색
vim.keymap.set('n', '<leader>fg', builtin.live_grep, {}) -- 단어 검색
vim.keymap.set('n', '<leader>fb', builtin.buffers, {}) -- buffer 탭 검색
vim.keymap.set('n', '<leader>fh', builtin.help_tags, {})
vim.keymap.set('n', '<C-p>', builtin.git_files, {})
end
}
init.lua
require('lazy').setup({
require 'plugins.theme',
require 'plugins.gitPlugin'
require 'plugins.telescope' -- 추가
})

이부분도 kickstart에서 가져온 것입니다. 하지만 이해할 수 있습니다. 단어, 파일이름, 작업 중인 버퍼로 검색할 수 있습니다.

tabs

neovim은 탭이 안 보입니다. 일반적인 에디터를 경험한 사람바로 보입니다. 하지만 neovim은 플러그인으로 보이도록 설정해야 합니다.

tabs

이 플러그인은 제가 doc을 검색하면서 설정했습니다. 탭을 보통 클릭으로 이동하는데 저는 보이는 탭 기준으로 키맵으로 이동하게 만들었습니다. 물론 telescope으로 버퍼 검색하고 이동하는 것이 가능합니다. 저는 버퍼가 많아지면 자동으로 닫기 기능도 추가하고 싶었습니다.

lua/plugins/tabs.lua
return {
'akinsho/bufferline.nvim',
version = "*",
dependencies = 'nvim-tree/nvim-web-devicons',
init = function()
vim.keymap.set("n", "bs", vim.cmd.BufferLineCloseOthers)
--vim.keymap.set("n", "<leader>bcr", vim.cmd.BufferLineCloseRight)
--vim.keymap.set("n", "<leader>bcl", vim.cmd.BufferLineCloseLeft)
vim.keymap.set("n", "bct", vim.cmd.BufferLinePickClose)
vim.keymap.set("n", "bt", vim.cmd.BufferLinePick) -- buffer target
end,
config = function()
local bufferline = require('bufferline')
vim.opt.termguicolors = true
require("bufferline").setup {
options = {
mode = "buffers",
style_preset = bufferline.style_preset.minimal,
themable = true,
numbers = "ordinal",
close_command = "bdelete! %d", -- can be a string | function, | false see "Mouse actions"
right_mouse_command = "bdelete! %d", -- can be a string | function | false, see "Mouse actions"
left_mouse_command = "buffer %d", -- can be a string | function, | false see "Mouse actions"
middle_mouse_command = nil, -- can be a string | function, | false see "Mouse actions"
indicator = {
icon = '▎', -- this should be omitted if indicator style is not 'icon'
style = 'underline', -- 'icon' | 'underline' | 'none',
},
buffer_close_icon = '󰅖',
modified_icon = '●',
close_icon = '',
left_trunc_marker = '',
right_trunc_marker = '',
diagnostics = "nvim_lsp",
offsets = {
{
filetype = "NvimTree",
text = "File Explorer", -- "File Explorer" | function ,
text_align = "center", -- "left" | "center" | "right"
separator = true
}
},
padded_slant = true,
color_icons = true, -- whether or not to add the filetype icon highlights
get_element_icon = function(element)
-- element consists of {filetype: string, path: string, extension: string, directory: string}
-- This can be used to change how bufferline fetches the icon
-- for an element e.g. a buffer or a tab.
-- e.g.
local icon, hl = require('nvim-web-devicons').get_icon_by_filetype(element.filetype, { default = false })
return icon, hl
-- or
--local custom_map = {my_thing_ft: {icon = "my_thing_icon", hl}}
--return custom_map[element.filetype]
end,
diagnostics_indicator = function(count, level, diagnostics_dict, context)
local icon = level:match("error") and " " or ""
return " " .. icon .. count
end,
separator_style = "thin", -- "slant" | "slope" | "thick" | "thin" | { 'any', 'any' },
hover = {
enabled = true,
delay = 200,
reveal = { 'close' }
},
show_buffer_icons = true, -- true | false, -- disable filetype icons for buffers
show_buffer_close_icons = true, -- true | false,
show_close_icon = true, --true | false,
show_tab_indicators = true, --true | false,
},
}
end
}
init.lua
require('lazy').setup({
require 'plugins.theme',
require 'plugins.gitPlugin'
require 'plugins.telescope'
require 'plugins.tabs' -- 추가
})

리팩토링

원래 이것도 관심사별로 분리한 것인데 아쉬운 점이 있습니다.

init.lua
require('lazy').setup({
require 'plugins.theme',
require 'plugins.gitPlugin'
require 'plugins.telescope'
require 'plugins.tabs'
})

뭐 사용할지 말지 제어하는 관점에서 좋을지 몰라도 저는 제가 사용중이라는 것을 매번 입력하기 귀찮습니다. 또 실수할 가능성도 있습니다. 그래서 먼저 저희는 위 호출하는 지점을 정리하겠습니다.

init.lua
require("lazy").setup("plugins")

이렇게 교체하면 됩니다. 상당히 단순합니다. 이렇게 두면 각자 원하는 곳에 관심사별로 모듈을 분리할 수 있습니다. 앞으로는 위처럼 설정하고 계속 이어집니다.

LSP 설정

lsp 없음

먼저 이해해야 하는 것은 LSP입니다. Language Server Protocol의 약어입니다. Language는 프로그래머가 프로그래밍 하는 언어에 해당합니다. 이 언어는 확장자를 통해서 구분하는 경우가 많습니다. js로 끝나면 자바스크립트라는 것을 알 수 있습니다. 프로그래머는 프로그래밍을 합니다. 예를 들어 자바스크립트 프로그래밍을 하면 js 확장자를 갖고 있는 문서를 작성하는 것입니다. 결과적으로 모든 프로그램은 문서입니다. LSP는 이 문서를 효율적으로 작성할 수 있게 해줍니다. Language Client는 우리가 작성하는 프로그램 문서이고 Sever는 Client의 요청에 맞게 제공합니다. 이 Protocol은 직역하면 규약입니다. 주로 우리는 확장자로 규약을 정할 것입니다. 자바스크립트 확장자라면 console 객체가 네이티브 객체라는 것을 프로그래머는 자주사용해서 압니다. 이 console 전체를 이해할 수 없어도 부분만 요청해도 남은 완성된 형태를 응답하는 것이 language server입니다. 이 네이티브 객체에 대한 정보를 갖고 있어서 console에 달려있는 메서드도 알 것입니다. console.log도 많이 사용하지만 계층구조가 많아지면 console.trace 메서드도 사용하는 것처럼 language server는 개발자의 생산성을 위해 알고 있습니다.

저는 포괄적으로 syntax highlighting, 자동완성, 저장에 포멧팅, linting에 대한 설정들입니다. 이과정은 사실 어렵게 생각하면 어렵고 쉽게 생각하면 쉽습니다. 막짜면 돌아게 만들 수 있지만 설정을 유지보수하기 어렵습니다. 어디에 무엇을 추가해야 무슨 언어에 어떻게 적용되는지 모르기 때문입니다. 반대로 어렵게 접근하면 분석마비에 걸릴 수 있습니다. 꽤 많고 다양한 플러그인을 활용해야 하기 때문입니다. 본인은 개발분야에서 특정분야에 해당하는 설정만하고 그외 데이터와 관련된 python, 로우레벨을 위한 rust 혹은 zig를 위한 설정도 하고 싶을 것입니다. 최소한 저는 설정하고 싶습니다.

코드 작성에서 제일 중요한 부분에 해당하기 때문에 이 과정을 천천히 확인하고 작업하는 것이 좋습니다.

개인적으로 가장 저의 취향과 비슷한 튜토리얼을 활용할 것입니다. 튜토리얼 혹은 해당 레포를 보고 분석하고 활용해 됩니다.

Neovim for Newbs. FREE NEOVIM COURSE이 튜토리얼이고 cpow/neovim-for-newbs이 레포입니다.

이 과정에서 다양한 시행착오를 했습니다. 하나는 kickstart 레포를 분석하고 설정들을 가져오는 것이었습니다. 그리고 kickstart를 버리고 모두 스스로 만들면서 LSP 설정을 어떻게 할 수 있는지 찾고 있었습니다. 그 과정에서 lsp-zero를 발견하고 설정을 했었지만 관심사 분리가 어려웠습니다. 그래서 설정 참고만 하고 결국에는 제거했습니다. 마지막으로 튜토리얼을 발견했습니다. 플러그인 매니저부터 비슷해서 많이 가져왔습니다.

다음은 플러그인 후보군입니다.

  • lsp-zero
    • 먼저 실수입니다. 무조건적으로 lsp-zero가 필요하지는 않습니다. 하지만 앞으로 성장하기 기대하고 있는 플러그인입니다. 프로그래머 입장에서 syntax highlighting, 자동완성, 저장에 포멧팅, linting은 한번에 하고 싶은 경우가 많습니다. 지금은 일부는 자동포멧팅을 지원하고 일부는 자동포멧팅을 지원하지 않습니다. 목적은 추상화이지만 추상화를 하는데 아직 부족한 점이 많습니다. 이런 이유로 제거 했습니다.
    • lsp-zero는 IDE랑 비슷한 syntax highlighting, 자동완성, 링팅, 저장시 포맷팅을 설정을 한번에 할 수 있게 만들기 위해 존재합니다. 우리는 코드로 우리 에디터를 설정하는데 이 설정에 대해서 추상화를 제공하는 플러그인입니다.
    • 하지만 자주 사용하는 키바인딩은 활용할 것입니다. 타입의 정의와 구현정의를 구분하고 접근한다는 점이 좋습니다.
  • mason & mason-lspconfig
    • mason은 LSP를 설치, 삭제, 갱신하기 좋은 플러그인입니다. 또 lsp 연결할 수 있는 플러그인도 같이 설치할 것입니다.
  • treesitter & treesitter/playground
    • 내부적으로 elisp를 사용합니다. emacs 유저들이 elisp를 사용하는데 그부분을 neovim이 사용할 수 있게 합니다.
    • 원래 LSP 설정을 위해 사용했지만 지금은 하이라이팅을 위해 사용할 것입니다.
  • cmp & cmp-lsp
    • 자동완성을 지원하는 라이브러리입니다. 자동완성에 대한 설명도 포함되어 있습니다.
    • lsp 설정을 같이 연결해야 자동완성이 됩니다.
  • none-ls
    • 원래는 null-ls가 존재했습니다. 하지만 아카이브 결정이 나고 다른 사람이 fork하고 유지보수하게 되었습니다. 기존 레포들은 이 플러그인으로 설치할 주체만 바꿔야 할것입니다. 운이 좋게 우리는 후발자주라 일단은 none-ls를 사용하고 나중에 더 좋은 플러그인이 나왔을 때 교체하면 됩니다.
    • 목적은 린트랑 포맷팅을 lsp에 연결하기 위한 플러그인입니다.

treesitter

OG lsp입니다. 사실 정확히는 LSP는 아닙니다. 신텍스 하이라이트를 제공하는 플러그인입니다. 또 자동 인덴트도 제공합니다. 우리가 사용하는 nvim-treesitter는 treesitter를 neovim에 사용할 수 있게 해주는 플러그인입니다. treesitter는 파싱생성 툴이고 점진적인 파싱 라이브러리입니다. 좋은 성능으로 AST를 만듭니다.

저는 비전공자이고 컴퓨터 언어론 수업을 들어본적이 없어서 내부를 모릅니다. 하지만 이것은 압니다. elisp로 작성되어 있습니다. emacs가 내부적으로 사용하는 언어를 사용합니다. 잘 활용하면 임베디드 쿼리도 자동완성을 흉내내게 만들 수 있습니다.

highlight.lua
return {
-- :TSInstallInfo 로 설치 정보 확인
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
dependencies = {
'nvim-treesitter/nvim-treesitter-textobjects'
},
config = function()
local config = require("nvim-treesitter.configs")
config.setup({
-- 하이라이트할 언어 추가
ensure_installed = { "lua", "vim", "vimdoc", "c", "cpp", "rust", "html", "css", "javascript", "typescript", "zig" },
highlight = { enable = true },
indent = { enable = true },
})
end
}

지금은 ensure_installed에서 앞으로 작성할 언어를 위한 지원하도록 설정했습니다. 여기서 주의할 점은 하이라이트랑 lsp랑은 별개의 것입니다.

사실 설정 후에 큰변화를 아직 못 체감할 수 있습니다.

mason

mason의 목적은 LSP 설치 혹은 삭제입니다. 순수하게 mason만 설치하는 것은 아니고 mason-lspconfig로 LSP를 설정도 같이 합니다. mason에 대해서는 아직 파일 분리를 안 하는 것이 좋을 것 같습니다.

mason-lspconfig의 목적은 neovim이 제공하는 lspconfig 사이 간극을 줄이는 것입니다.

클라이언트와 서버는 놀랍게도 RPC 통신을 하고 있다고 합니다. 사실 잘 모릅니다.

neovim의 경우 lsp를 설치하고 설치한 LSP가 통신할 수 있게 별도로 설정해줘야 합니다.

mason을 사용할 때 주의할 점은 LSP만 설치하지 않습니다. 다른 DAP, Linter도 설치가 가능합니다. 하지만 DAP를 설치하고 LSP가 동작 안한다고 실수할 가능성이 있습니다. 제가 그 실수를 했습니다.

-- :Mason 명령으로 확인하지만 설정은 코드로 합니다.
return {
{
'williamboman/mason.nvim',
config = function()
require("mason").setup()
end
},
{
'williamboman/mason-lspconfig.nvim',
config = function()
require("mason").setup({
ensure_installed = { "lua_ls", "rust_analyzer", "clangd", "cssls", "eslint", "tsserver", "html" },
})
end
},
{
'neovim/nvim-lspconfig',
}
}

여기까지는 그져 lsp를 설치한 것에 불과합니다. 다음은 설치한 lsp를 연결해야 합니다. 서버를 설치하면 서버를 실행해야 하는 것과 같습니다.

	-- :LspInfo 로 LSP 설정 확인 가능
-- :h vim.lsp.buf 현재 버퍼의 도음말도 찾는 것이 가능합니다.
{
'neovim/nvim-lspconfig',
config = function()
local lspconfig = require('lspconfig')
lspconfig.lua_ls.setup({})
vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
end,
}

LSP 설정은 위처럼해야 서버가 연결됩니다. K를 누르면 hover해서 보일 설명이 보이게 됩니다. 역시 훌륭한 vim 유저답게 마우스를 멀리합니다.

Hover

여기까지는 LSP 연결만 한 것 뿐입니다. LSP를 연결하면 자동완성을 연결해줘야 합니다. 이 작업은 별개의 것입니다. 또 정의로 이동, 타입정의로 이동, 호출로 이동은 나중에 다루겠습니다. 이것은 lsp-zero를 보고 발견했습니다.

cmp

자동완성입니다. 개발자 생산성의 상당히 큰 부분입니다.

자동완성

해당 레포에서 설명을 자세히 확인하는 것이 중요합니다. LSP 설정에서 자동완성이 가장 어려운 부분 중 하나입니다. 현재까지 이 자동완성을 해결해보기 위해 lsp-zero 같은 플러그인이 도전했지만 저에게는 설득하는데 실패했습니다. 설명을 보고도 이해하기 상당히 어려울 것입니다.

자동완성에 중요한 플러그인은 nvim-cmp입니다. 자동완성을 해주는 엔진에 해당합니다. cmp-nvim-lsp이 현재 버퍼의 LSP랑 자동완성 플러그인인 nvim-cmp와 연결을 해줍니다. 그리고 자동완성 엔진에 스니펫을 확장해야 합니다. 스니펫은 LuaSnip이 지원해줍니다. neovim이 스니펫을 가질 수 있게 해주는 엔진에 불과해서 실제 스니펫을 friendly-snippets으로 가져와야 합니다. 이렇게 가져온 스니펫을 현재 자동완성과 같이 연결하기 위해 cmp_luasnip이 있는 것입니다.

return  {
-- Autocompletion
'hrsh7th/nvim-cmp',
dependencies = {
-- Snippet Engine & its associated nvim-cmp source
{
'L3MON4D3/LuaSnip',
build = (function()
-- Build Step is needed for regex support in snippets
-- This step is not supported in many windows environments
-- Remove the below condition to re-enable on windows
if vim.fn.has 'win32' == 1 then
return
end
return 'make install_jsregexp'
end)(),
},
'saadparwaiz1/cmp_luasnip',

-- Adds LSP completion capabilities
'hrsh7th/cmp-nvim-lsp',
'hrsh7th/cmp-path',

-- Adds a number of user-friendly snippets
'rafamadriz/friendly-snippets',
},
}

위는 vim kickstart의 예시입니다.

lua/plugins/autocomplete.lua
return {
{
"hrsh7th/cmp-nvim-lsp"
},
{
"L3MON4D3/LuaSnip",
dependencies = {
'saadparwaiz1/cmp_luasnip',
'rafamadriz/friendly-snippets'
}
},
{
"hrsh7th/nvim-cmp",
config = function()
-- Set up nvim-cmp.
local cmp = require 'cmp'
require("luasnip.loaders.from_vscode").lazy_load()

cmp.setup({
snippet = {
-- REQUIRED - you must specify a snippet engine
expand = function(args)
require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
}),
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'luasnip' }, -- For luasnip users.
}, {
{ name = 'buffer' },
})
})
end
}
}

위는 튜토리얼에서 소개한 래포에서 가져온 예시입니다. 여기서는 sources에 할당된 2가지 설정이 중요합니다. 먼저 { name = 'nvim_lsp' } 설정은 자동완성을 지원하게 해줍니다. { name = 'luasnip' }은 확장한 스니펫을 연결할 수 있게 해줍니다.

아직 끝난 것은 아닙니다. 최종적으로 lsp랑 연결이 필요합니다.

return {
'neovim/nvim-lspconfig',
config = function()
local capabilities = require('cmp_nvim_lsp').default_capabilities()

-- @todo lsp attach hook 적용
local lspconfig = require('lspconfig')
lspconfig.lua_ls.setup({
capabilities = capabilities
})
lspconfig.clangd.setup({
capabilities = capabilities
})
lspconfig.tsserver.setup({
capabilities = capabilities
})
lspconfig.volar.setup({
capabilities = capabilities
})

-- @todo 키맵 추가
vim.keymap.set('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
end,
}

여기서 capabilities를 설치한 LSP별로 역어 두면 LSP가 지원하는 자동완성을 활용할 수 있게 될 것입니다.

null-ls를 대체할 none-ls

neovim 커뮤니티에 중요한 사건이 발생했습니다. null-ls가 nli-ls라고 명명하지 않은 것입니다. undefined-ls가 아닌 것을 감사히 여기십시오 아니 메인테이너가 더이상 유지보수하는데 포기 선언한 것입니다. 주로 neovim을 더이상 사용하지 않는다고 했습니다. 참 신기합니다.

IMPORTANT: Archiving null-ls

뭐 일단 레포는 아카이브된다고 합니다. 상당히 훌륭한 플러그인인데 nvimtools 팀이 유지보수하기 시작했습니다. 많은 distro가 의존하고 있기 때문에 대신 커뮤니티에서 유지보수하게 된 것입니다.

return {
"nvimtools/none-ls.nvim",
config = function()
local null_ls = require("null-ls")
null_ls.setup({
sources = {
null_ls.builtins.formatting.stylua,
null_ls.builtins.diagnostics.eslint_d,
null_ls.builtins.formatting.prettierd,
null_ls.builtins.completion.spell,
},
})
vim.keymap.set('n', '<leader>gf', vim.lsp.buf.format, {})
end
}

사실 설정 자체는 단순합니다. 하지만 알아야 할 개념은 복잡합니다. 원래 LSP랑 린터와 포맷터는 별개의 것입니다. 대략 설명해보자면 코드를 작성하는 중간에 CLI 툴로 설정한 포멧에 맞도록 실행하고 처리하도록 합니다. 이러한 기능을 null-ls가 설정이 쉽도록 해줍니다. null-ls는 Mason에 의존해 해당하는 린터로 분석하고 포멧터로 원하는 시점에 실행하게 만들 수 있습니다. 지금은 파일을 작성하는 중간에 포멧팅하도록 설정되어 있지만 원하면 파일 저장 시점에 포맷팅하도록 설정도 가능합니다.

유용한 키맵 설정

리더키 설정

vim.g.mapleader = " "
vim.g.maplocalleader = " "

탭설정

How can I set my TAB key to be 4 spaces indent?

tab 대신에 space 4칸으로 수정 하는 방법입니다.

-- 1 tab을 2 space로 변환
vim.opt["tabstop"] = 2
vim.opt["shiftwidth"] = 2

-- 2칸
vim.opt["numberwidth"] = 2

라인넘버 현재 기준으로 상대 단위로 표시

-- 현재 줄기준으로 표시
vim.wo.relativenumber = true

탐색 설정

vim.keymap.set("n", "<leader>ex", vim.cmd.Ex)

결론

이제는 C언어 프로그래밍을 밀어둘 변명이 없어졌습니다. 큰일났군요

여러분에게 vim을 추천하겠는가? 저는 절대 추천하지 않을 것입니다. 저 혼자 이 간지라는 꿀을 빨기 위해서 추천 안할 것입니다. 이런 이유말고 neovim 또한 하나의 생태계입니다. 또 업무용 프로그래밍 언어를 타입스크립트를 사용하는데 얼마나 많은 변화가 있는지 압니다. 즉 변화가 있다는 것은 유지보수가 필요하다는 것입니다.

본인의 개발환경을 본인이 사용하는 것위주로 설정하면서 현대 IDE가 얼마나 많은 것들을 대신해주는지 배울 수 있습니다. 또 본인이 필요한 것만 설정하면서 에디터가 상당히 가벼울 수 밖에 없습니다. 이런점은 사실 큰 장점은 아닙니다. 개발자에게는 과정은 별로 안 중요합니다. 정확히 회사 입장에서 개발하는 과정은 별로 안 중요하고 결과로 만들어진 것만이 중요합니다. 회사에서 사용하는 에디터가 아무리 난해해도 결국에 더 생산성이 높으면 결국 그 에디터를 사용해야 합니다. 또 무엇보다 보안문제가 발생할 여지가 많습니다.