Neovim에서 LSP 설정에 쓰이는 플러그인들의 역할

Posted on November 25, 2024

Language Server Protocol 이라고 이미 프로토콜 이름에 Server가 들어가 있어서 살짝 혼란스러울 수 있습니다. 헛갈리지 않게 줄여서 LSP라 부르겠습니다. 네오빔은 LSP 서버와 내장 LSP 클라이언트가 소통할 수도 있고, CoC 플러그인을 이용해 소통할 수도 있습니다. ※ 네오빔을 쓰시는 주변 분들을 보니, 이제(2024.11) CoC보다는 빌트인된 클라이언트를 많이 쓰시는 것 같긴 한데, CoC 플러그인 개발이 끊긴 건 아닙니다.

여기서는, 설치 방법이 아니라, 내장 클라이언트를 쓸 때 관련 플러그인들이 무슨 역할을 하는 지만 봅니다.

LSP 서버는 소스 코드의 의미semantics를 분석해서

등의 작업을 지원합니다.

네오빔은 LSP클라이언트를 내장하고 있고, 언어에 맞는 각 서버들은 써드 파티를 이용합니다.
(아래는 네오빔 도움말과, 각 플러그인들의 설명을 참고해서 정리했습니다.)

수작업

-- 파일 타입에 따라 자동으로 실행될 이벤트 핸들러 FileType autocommand
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',  -- 여기 예시는 파일 타입이 "python"일 때 실행한다는 뜻
  callback = function(args)
    vim.lsp.start({
      name = 'my-server-name',
      cmd = {'언어 서버 실행명', '--option', 'arg1', 'arg2'},
      -- 전체 코드의 의미를 파싱해야 하니, 어디가 프로젝트 루트인가 알아야 합니다.
      root_dir = vim.fs.root(args.buf, {'setup.py', 'pyproject.toml'}),
      -- on_attach = ... 
      -- LSP 클라이언트가 현재 버퍼와 붙을 때 실행할 작업을, 이 핸들러에 지정합니다.
      -- 키 바인딩을 한다거나 다른 플러그인들과 연동 작업 같은 것들 넣기에 적당합니다.
    })
  end,
})

vim.fs.root({source},{marker}) 함수로 상대 경로를 따라 올라가며 루트 디렉토리를 찾습니다. 프로젝트의 루트임을 알 수있게 해주는 파일들을 지정합니다. 위에선 setup.py파일이나 pyproject.toml이 있는 폴더를 찾는다는 뜻입니다. 실행한 언어 서버와 소통을 담당할 클라이언트를 시작하고 현재 버퍼와 연결합니다.

버퍼에 클라이언트가 연결되면, 네오빔이 자동으로 다음 작업을 합니다.

위 설정들을 자동으로 하는데, 사용자화 하려면, LspAttach, LspDetach 자동실행명령어autocommand를 이용할 수 있습니다.

위처럼 단지 vim.lsp.start(...) 해 주는 것만으로 단일 언어를 위한 LSP 설정을 마칠 수 있습니다만, 언어에 따라, 확장자 별로 위 작업을 모두 반복해서 하려면 꽤 성가신 반복 작업입니다. 보통 아래와 같이 플러그인들을 활용합니다.

플러그인 활용

힘을 합쳐

mason.nvim

mason.nvim: LSP 서버, DAP서버, 린터, 포맷터를 설치, 관리합니다. 패키지들은 네오빔 데이터 디렉토리(:h standard-path)에 설치됩니다. 실행 파일들은 bin/ 한 곳에 심볼릭 링크를 만들어 두고, 이 경로를 네오빔의 PATH에 추가합니다. LSP 클라이언트와 소통할 외부 서버들 관리를 위한 패키지 매니저라 볼 수 있습니다.

nvim-lspconfig

lspconfig: 다양한 LSP서버들의 기본적인 설정을 제공합니다.
예시) 하스켈의 LSP서버 hls를 위한 설정으로, 플러그인 설치 후 nvim-lspconfig/lua/lspconfig/configs/hls.lua를 보면, 다음과 같이 되어 있습니다. 위에서 본 vim.lsp.start에 넣어 줄 옵션 값들입니다. 특별한 동작을 한다기보다 언어별 설정값을 저장해 둔 데이터 셋트입니다.

local util = require 'lspconfig.util'

return {
  default_config = {
    cmd = { 'haskell-language-server-wrapper', '--lsp' },
    filetypes = { 'haskell', 'lhaskell' },
    root_dir = util.root_pattern('hie.yaml', 'stack.yaml', 'cabal.project', '*.cabal', 'package.yaml'),
    single_file_support = true,
    settings = {
      haskell = {
        formattingProvider = 'ormolu',
        cabalFormattingProvider = 'cabalfmt',
      },
    },
  },

nvim-lspconfig/lua/lspconfig/manager.lua 에서

131 local client_id = lsp.start(new_config, {
    ...

LSP서버에 따라 적당한 옵션을 골라 lsp.start(vim.lsp.start)를 실행합니다.

반드시 mason과 짝지어 돌리지 않아도 됩니다. 직접 외부 LSP서버를 설치하고, require('lspconfig').pyright.setup({})와 비슷한 코드로 개별 설정을 할 수도 있습니다.

mason-lspconfig

mason-lspconfig: mason.nvimlspconfig, 이 둘을 연결해 줄 브릿지입니다. mason.nvim으로 원하는 외부 서버를 설치하면, 해당 서버에 맞는 설정을 lspconfig가 넣어줘야 자동 LSP 환경 설정이 마무리 됩니다. mason.nvim이 설치한 서버를 보고, lspconfig의 언어별 setup {}을 부르는 역할을 합니다.

fidget.nvim: 가끔 LSP 동작이 잘되는 것인가 의심스러울 때가 있습니다. 필수는 아니지만 이 플러그인을 설치하면 LSP 동작 상태를 실시간으로 볼 수 있습니다.

lspsaga.nvim: - 지금 어느 파일의 어느 함수에 있나 보여주는 Breadcrumbs가 생기고, - 함수 호출 계보를 보여주고, - 빌트인 코드 액션은 줄 단위 진단diagnostic을 하고, Lspsaga는 커서 단위 진단을 합니다.

빌트인 클라이언트와 일부 기능이 겹치지만, 쓸만합니다.

※ 얼마 전(2024.10) lua LSP 서버의 이름이 바뀌면서 lua_ls란 이름과 lua-language-server 이름을 매핑해주는 작업을 여기서 해줬었습니다.

마무리

보통, 위 플러그인들 중 하나를 쓰는 게 아니라, mason.nvim, lspconfig, mason-lspconfig를 같이 설치해서 연동해서 씁니다. 한 두 개의 언어만 고정해서 쓴다면, 수작업으로 설졍해도 되지만, 여러 언어들 관련 설정을 하기에는 수작업은 많이 성가십니다. 위 플러그인들을 이용해서 설정하다가 문제가 생기면, Vim.kr(한국 빔 사용자 모임) 디스코드 로 오셔서 도움을 받으실 수 있습니다.


  1. :set omnifunc?로 설정값을 확인 할 수 있습니다. v:lua.vim.lsp.omnifunc 값이 나오면 LSP 옴니 모드 완성Omni mode completion이 활성화된 상태입니다. 단순한 텍스트 매칭으로 완성될 단어를 제안하는 일반 텍스트 완성과 다르게, 컨텍스트(어떤 형식의 파일인지, 현재 코드의 상태는 어떤지, 함수 호출 위치가 어디인지…)에 따라서 완성될 단어를 제안합니다.
    (다양한 언어, 문맥을 지원한다고 해서 Omni가 붙었다고 하는데, 모두를 뜻하는 Omni가 맞나 싶습니다.)
    nvim-cmp 플러그인을 이용해서 여러 소스를 참고해서 제안하도록 할 수 있습니다.↩︎

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com