vim 플러그인 만들기

vim 스크립트로 작성한 유용한 기능을 플러그인으로 만드는 방법을 알아봅시다.

vim 플러그인 만들기
Photo by Mario Häfliger / Unsplash

vim에서 '어떤 기능을 추가'하는 것은 크게 세 가지 정도로 구현됩니다. 먼저 일련의 반복적인 동작을 하나의 키 맵으로 정의하는 것이 있습니다. 두 번째로 사용자 정의명령을 만들어서 vim의 명령줄 명령이나 스크립트를 손쉽게 실행하는 방법이 있습니다. 세 번째로는 사용자 정의 함수를 작성하여, 보다 복잡한 기능을 구현하는 방법이 있습니다.

그런데 이 방법들은 단독으로 사용되기 보다는 함께 사용되는 경향이 있습니다. 복잡한 기능을 구현하기 위해서는 vimscript로 사용자 정의 함수를 사용하지만, :call MySpecialFunction() 과 같은 식으로 직접 함수를 호출하는 명령을 입력해서 사용하는 일은 거의 없습니다. 대부분은 간단한 이름의 사용자 정의 명령으로 함수를 호출하거나, 약속된 키 맵을 따로 사용하는 경우가 대부분이죠.

어떤 새로운 기능을 구현했을 때, 함수와 명령, 키 맵을 모두 정의한다면 이것들은 하나의 세트를 이루게 됩니다. 이럴 때, 이 세트만 별도의 파일로 만들어 둘 수 있다면 나의 다른 컴퓨터나 다른 사람의 컴퓨터에도 손쉽게 설치가 가능할 것입니다. 즉, 하나의 새로운 vim 플러그인을 만들 수 있는 것이죠.

요즘 인기 있는 플러그인들은 모두 플러그인 관리자를 통해서 설치하는 경우가 많습니다만, 간단한 플러그인의 경우에는 파일 하나에 기능을 모두 담을 수 있을 겁니다.

vim 플러그인은 기본적으로 런타임 디렉토리 중 하나의 plugin 디렉토리 아래에 있는 .vim 파일입니다. 여기에 플러그인 파일을 작성해두면 vim은 시작될 때 플러그인 디렉토리를 모두 뒤져서 플러그인들을 읽어들입니다.

그러면 지난 글에서 소개했던 스크립트를 플러그인 형식으로 만들어서 사용하는 방법에 대해 살펴보겠습니다.

vim에서 패턴에 매칭되는 영역을 추출하기
vim에서 패턴에 매칭되는 부분을 변경하는 것이 아니라, 매칭되는 부분만 남기고 나머지를 제거하는 방법

런타임 디렉토리

vim의 기능과 동작에 영향을 주는 여러 설정들은 '런타임 디렉토리'에 저장됩니다. 런타임 디렉토리는 다시 시스템 런타임 디렉토리와 사용자 런타임 디렉토리로 나뉩니다. 시스템 런타임 디렉토리는 vim이 설치되어 있는 디렉토리이고, 사용자 런타임 디렉토리는 보통 사용자 홈 아래에 /.vim, %USERPROFILE%\vimfiles 이런 경로로 있습니다. vim에서 :echo $MYVIMDIR을 실행해서 확인할 수 있습니다.

런타임 디렉토리는 다시 아래의 구조를 가집니다. 모든 디렉토리를 가지고 있을 필요는 없습니다.

~/.vim/
├── autoload/     # 자동으로 로드되는 함수들
├── colors/      # 컬러 스키마 파일들
├── compiler/    # 컴파일러 설정
├── doc/         # 도움말 문서
├── ftplugin/    # 파일타입별 플러그인
├── indent/      # 파일타입별 들여쓰기 설정
├── keymap/      # 키보드 매핑
├── lang/        # 다국어 메시지
├── plugin/      # 일반 플러그인
├── syntax/      # 문법 강조 파일
└── tutor/       # Vim 튜터리얼

몇 가지 디렉토리에 대해 설명하면 다음과 같습니다.

  1. ftplugin : 특정한 타입의 파일이 열렸을 때, 파일 타입과 이름이 같은 스크립트가 실행됩니다.
  2. plugin : vim이 시작될 때 로딩될 플러그인 파일들을 이 곳에 보관합니다.
  3. autoload : 선택적으로 로딩해도 되는 함수들은 이곳에 작성합니다. 함수가 호출될 때 로딩하여, vim의 초기 로딩 속도를 빠르게 하는데 도움이 됩니다.

플러그인의 기능을 키맵으로 사용하기

플러그인에서 제공하는 함수를 키 맵으로 호출하려고 할 때, 고려해야 하는 점이 있습니다. 먼저 직접 작성한 함수가 다른 곳에서 정의한 함수와 이름이 충돌할 수 있는 경우가 있습니다.

겹치는 이름이 없도록 주의깊게 짓는 여러 가지 방법(?)이 있을 수도 있지만, 플러그인으로 제작한 파일을 다른 사람이 가져다 쓰는 경우에는, 그 동네에서는 겹치지 말라는 법은 또 없죠. 함수 이름도 함수 이름이지만, 키 맵 역시 마찬가지입니다. 이러한 이름 충돌을 피하기 위해서는 우선 함수 이름은 전역으로 정의하지 않아야 하고, 플러그인 내에서 플러그인 키맵을 만든 후, 사용자가 직접 자신의 키맵을 플러그인 키맵에 맵핑하도록 하는 방법을 사용하면 됩니다.

아래의 코드를 먼저 보겠습니다.

vim9script

def ExtractSearchedText()
	if @/ == ''
		echo "There is no search pattern."
		return
	endif
	var temp = @a
	@a = ''
	silent! :%s//\=setreg('A', submatch(0), 'al')/gn
	if len(@a) > 0
		new
		put! a
		normal gg
	else
		echo "No matched."
	endif
	@a = temp
	nohls
	redraw
enddef


nnoremap <Plug>(extract-searched) <Cmd>call <SID>ExtractSearchedText()<CR>

맨 아래에 스크립트에서 작성한 함수를 호출하는 키 맵을 정의했습니다.

  1. <Plug>라는 예약어는 일반적인 키로 입력할 수 없는 문자로 대체되고, 입력된 경우에는 오른쪽의 키 시퀀스로 변경됩니다.
  2. <Cmd>:를 입력하여 명령줄 모드로 전환한다는 것을 좀 더 고상하게 표현한 것입니다.
  3. 함수 이름 앞에 <SID>를 붙인 것은 플러그인이 로드될 때 결정되는 스크립트ID로 치환됩니다. 따라서 다른 스크립트 파일에 같은 이름의 함수가 있더라도 겹치지 않습니다.

이 파일을 (이름은 아무거나 상관 없습니다.) plugin 디렉토리에 두고 vim을 다시 시작하면 플러그인이 로드됩니다. 기능을 호출하기 위해서는 $MYVIMRC 파일에 다음 키 맵핑을 추가합니다. 이 때 키 맵은 자신이 원하는 맵핑을 쓰면 됩니다.

nmap <leader>xz <Plug>(extract-searched)

중복 로딩 방지

plugin 디렉토리 하위에 다시 서브 디렉토리를 두고 파일을 구성하는 것도 가능합니다. 이 경우 디렉토리 구조 내에서 같은 이름의 파일이 여러 번 들어갈 수도 있습니다. 이 때에는 가장 먼저 로드되는 1개 파일만 로드됩니다.

ftplugin 디렉토리는 이와 다르게 이름이 같은 파일이 발견되면 누적하여 로드되며, 반복해서 로딩될 수도 있습니다. 중복 로딩을 방지하기 위해서, 보통은 다음의 테크닉이 사용됩니다.

vim9script

# 스크립트 최상단에 이미 로드되었는지 체크.
if exists('g:blahblahblah')
  finish
endif

g:blahblahblah = 1

Pack

패키지 기능은 vim8에 도입된 것으로, 서드 파티 플러그인 관리자 없이도 외부 플러그인들을 쉽게 설치할 수 있게 해줍니다. dist/, local/로 로컬에서 개발하는 플러그인과 다른 곳에서 배포된 플러그인을 구분하고, 다시 각각의 디렉토리는 start/, opt/ 로 구분하여 시작 시 로드될 것과 선택적으로 로드할 것들을 구분합니다.

이 아래에 각 디렉토리별로 구분된 플러그인들은 독립적으로 런타임 디렉토리 구조를 가질 수 있습니다. 따라서 git 명령으로 적절한 위치에 플러그인 저장소를 복제해두면 설치해서 사용할 수 있게끔 되어 있습니다.

~/.vim/      # 사용자 런타임 디렉토리 (Unix/Linux)
├── pack/                # 패키지 디렉토리 (Vim 8+)
│   ├── dist/           # 서드파티 플러그인용
│   │   ├── start/     # Vim 시작 시 자동 로드
│   │   │   ├── nerdtree/  # 각 플러그인 디렉토리
│   │   │   └── fugitive/
│   │   └── opt/      # :packadd로 선택적 로드
│   │       ├── coc/
│   │       └── youcompleteme/
│   │
│   └── local/         # 로컬 개발 플러그인용
│       ├── start/    # 자동 로드
│       │   └── myplugin/
│       └── opt/     # 선택적 로드
│           └── myoptionalplugin/
├── autoload/               # 필요할 때 로드되는 함수들
│   └── myutil/            # 네임스페이스로 사용되는 하위 디렉토리
│
├── ftdetect/             # 파일타입 감지 스크립트
│
├── ftplugin/             # 파일타입별 설정
│   ├── python.vim        # Python 전용 설정
│   ├── javascript.vim    # JavaScript 전용 설정
│   └── ruby/            # 디렉토리로도 구성 가능
│
└── plugin/            # 일반 플러그인
    ├── settings.vim  # 전역 설정
    └── mappings.vim  # 키 매핑