개인적으로 vim 설정을 하나의 vimrc 파일에 몰아서 만들기 보다는 여러 개의 파일로 쪼개어 사용자 설정 폴더 내에 배치해두고 사용하고 있다. 그러다보니 특정한 설정을 수정하려 할 때, 해당 파일을 빨리 찾아서 여는 기능이 필요했다. 사용자 정의 Ex 명령으로 만드는 방법이 있지만, Vim8의 팝업 기능을 사용하면 메뉴에서 원하는 파일을 선택해서 여는 기능을 구현할 수 있다.
이 기능을 구현하는 코드를 vim9 스크립트 버전으로 변경하여 공개한다.실제 함수는 autoload 디렉토리 아래에 만들어서 최초 실행하는 시점에 로딩되도록 하고, 맵핑만 vimrc 혹은 별도의 플러그인 폴더 내의 스크립트에서 정의한다. vim-plug 로 설치된 플러그인 파일은 검색에서 제외하기 위해서 특정한 디렉토리들만 지정해서 탐색하도록 할 것이다. 파일을 검색하고 목록을 만드는 기능을 구현하기 위해 필요한 vimscript 함수들은 다음과 같다. 자세한 내용은 vim 도움말 참고.
globpath()
: 특정한 위치에서 glob 패턴으로 파일을 검색하고 그 결과를 리턴한다. 이 함수는 기본적으로 찾은 파일의 경로를 ‘,’ 로 연결한 문자열로 돌려주지만, 네 번째 인자를v:true
로 넘기는 경우에는 리스트로 변환하여 반환한다. 참고로 찾을 경로 역시 ‘,’로 연결하면 여러 디렉토리에 대해 한 번에 검색할 수 있다.map()
: 리스트의 각 원소에 대해 주어진 함수나 람다식을 적용한다. 주의할 점은 이 함수는 원본 리스트를 변형하면서 그 원본을 리턴하는 함수이기 때문에, 맵핑을 적용한 사본을 사용하고자 한다면aList[:]->map()
과 같이 사본을 만들어서 적용하거나,mapnew()
함수를 사용해야 한다. vimscript의filter()
등의 함수는 모두 원본을 변형하니 주의가 필요하다.split()
,join()
: 문자열을 리스트로 쪼개거나, 쪼개진 리스트를 다시 하나의 문자열로 합치는 역할을 한다.sort()
: 리스트를 정렬한다. (원본을 변경한다.) 함수나 함수이름, 람다식을 전달하여 정렬 기준을 적용해줄 수 있다.match()
: 원래는 문자열에서 패턴을 검색하는 함수인데, 리스트에서 특정한 원소를 찾을 때에도 사용한다. 여러모로 자바스크립트의indexOf()
와 비슷하다.
파일 목록 만들기
globpath()
함수를 사용하여 특정한 디렉토리에서 '**/*.vim'
이라는 패턴으로 검색하면 해당 디렉토리 및 그 하위의 모든 디렉토리에서 확장자가 .vim
인 모든 파일을 찾을 수 있다. 이 때 전달하는 경로에 여러 디렉토리를 동시에 전달할 수 있는데, 각각의 디렉토리를 콤마로 연결하여 전달하면 된다. 이 함수를 찾은 파일들의 경로를 문자열로 리턴하는데, 네 번째 인자를 v:tru
e 로 전달하여 문자열의 리스트로 받는 것이 가능하다.
vim9script
cost keys = '123456789ABCDEFGHIJKL'
const dirs = 'autoload plugin utils'->split()
->map((i, v) => '~/vimfiles/' .. v .. '/')
->join(',')
var files: list<string> = globpath(dirs, '**/*.vim', v:true, v:true)
->sort((a, b) => {
var [x, y] = map([a, b], (i, v) => fnamemodify(v, ':t')->lower())
return x > y ? 1 : -1
}
files
는 주어진 이름의 디렉토리들을 ~/vimfiles/
아래에 연결해서 여기서 vim 파일을 찾은 다음, 파일 이름의 순서대로 정렬한 목록이다. 나중에 keys
의 각 글자와 파일의 이름만 짝을 지어 메뉴의 항목들을 생성하고, 선택된 인덱스를 사용해서 열어야 할 파일의 경로를 알아낼 수 있게 한다.
메뉴 팝업 관련 vimscript 함수
메뉴는 팝업 기능의 일종이다. popup_menu()
를 사용하여 메뉴를 띄울 수 있다. vim에서 메뉴가 표시되고 있는 경우, 키 입력의 우선순위는 메뉴 팝업이 가져간다. 기본적으로 j/k
키를 사용하여 메뉴 항목의 선택을 변경할 수 있고, 엔터키나 스페이스바를 눌러서 선택을 확정한다. 이 동작은 메뉴의 “필터함수”에 의해서 작동하는데, 디폴트로 popup_filter_menu()
함수가 사용되며 이 동작을 커스텀 (메뉴 항목에서 첫 글자를 입력해서 바로 선택하는 등)하고 싶다면, 별도의 함수를 정의하면 된다.
메뉴상에서 스페이스바나 엔터키를 누르면 선택을 완료한다. 이 판정을 popup_filter_menu()
에서 하게 되면, 선택이 완료된 키 입력 후에는 선택된 메뉴 항목의 인덱스(1부터 시작한다)을 사용해서 메뉴의 콜백 혹은 popup_close()
를 호출한다. 이 함수는 메뉴를 닫는 동작을 하는데, 만약 메뉴에 callback
속성 (호출가능한 함수)이 있다면 해당 함수를 호출하면서 사용자가 선택한 메뉴값을 전달해준다.
메뉴 선택 핸들러 정의하기
메뉴의 콜백은 메뉴의 id
와 메뉴에서 사용자가 선택한 항목의 인덱스(1부터 시작한다!)를 전달받아 호출된다. 이를 통해서 사용자가 어떤 선택을 했는지 알 수 있으므로, 그에 맞는 동작을 호출해주면 된다.
우리가 만들 메뉴 기능에서는 n 번째 메뉴가 선택되었을 때, 파일 목록에서 인덱스가 n – 1 인 경로의 파일을 열어주면 되는 것이다. 이 동작은 메뉴에 대한 콜백 함수로 전달되어야 하니 별도의 함수로 만든다. 이 함수는 메뉴의 id 값과 선택된 항목의 번호를 인자로 받게 된다. 참고로 선택된 번호는 1로 시작하기 때문에 목록에서 그에 대응하는 짝을 찾으려면 value - 1
을 사용한다. 취소하는 경우, 0을 받게 된다.
def QuickLink_Handler(id: number, value: number)
if value > 0
silent execute 'e ' .. paths[value - 1]
endif
enddef
메뉴 키 필터 정의하기
메뉴에서 j/k
키로 선택을 변경하는 것 외에 순번에 해당하는 키를 입력해서 항목을 바로 선택하게 하려면 메뉴 필터 함수를 추가로 정의하면 된다. 생성된 메뉴는 키 이벤트가 발생할 때마다 입력된 키를 자신의 filter
속성에 해당하는 함수에 호출한다. 디폴트로 popup_filter_menu()
가 지정되며, 이 함수는 j/k
에 대해서 선택항목의 포커스를 이동하고, 엔터/스페이스로는 현재 하이라이트된 항목을 선택하면서 메뉴를 닫는다. 그리고 Esc
키에 대해서는 선택을 취소한다. (이 경우에도 콜백 함수가 호출된다.)
커스텀 필터 함수는 popup_filter_menu()
와 유사하게 메뉴의 식별자와 입력된 키를 인자로 받는다. 해당 함수가 처리하지 않을 키인 경우에는 popup_filter_menu()
로 전달하여 위임하면 된다. 참고로 이 함수는 매번 v:true
를 호출해야 한다. 우리는 각 메뉴 항목에 대해서 바로 선택할 키를 keys
라는 변수에 문자열로 지정해 놓고, 해당 키가 눌려지면 바로 선택된 것으로 작동하게 할 것이다.
def QuickLink_Filter(id: number, key: string): bool
var i = match(keys, key)
if i > -1
popup_close(id, i + 1) # '1'의 인덱스는 0인데, 이 때 1을 넘겨주어야 한다.
return v:true
endif
return popup_filter_menu(id, key)
enddef
메뉴 호출하기
핸들러 함수들을 준비했으니, 메뉴를 호출하자. 이미 만들어놓은 files
에 keys
를 조합해서 메뉴 항목을 만들고, 이를 콜백, 필터 함수 옵션과 함께 popup_menu()
에 전달하면 된다. vimscript에는 파이썬의 zip()
과 같은 지퍼함수가 없기 때문에 mapnew()
를 사용해서 메뉴 항목을 생성하도록 한다. 이 데이터도 미리 만들어놓을 수 있겠지만, 그냥 vim9 모드에서는 빠르게 처리될 것이기 때문에 매번 생성하게 해봤다.
참고로 files
는 각 파일의 전체 경로를 담고 있기 때문에 fnamemodify()
함수에 ':t'
옵션을 주어 파일 이름만 떼어내어 항목을 만들도록 한다. 또 이 함수가 autoload 파일 외부에서 실제로 호출될 함수이기 때문에 export def
를 사용하여 함수를 정의해주도록 한다.
export def OpenQuickLinkMenu()
var names = files->mapnew((i, v) => keys[i] .. ':' .. fnamemodify(v, ':t'))
popup_menu(names, {callback: QuickLink_Handler, filter: QuickLink_Filter})
enddef
이렇게 해서 완성이다. 이제 vimrc 파일이나 그 외에 vim 구동 시 로딩되는 플러그인 등에서 사용자 정의 Ex 명령이나 키 맵을 사용하여 이를 호출하면 된다. 아래는 이 기능의 전체 코드와 별도의 플러그인에서 정의해야 하는 키 맵 사용 방법이다.
vim9script
# ~/vim/autoload/helper9.vim
const keys: string = '123456789ABCDEFGHIJKML'
const dirs: string = 'autoload ftplugin plugin utils'->split()
->map((i, v) => '~/vim/' .. v)
->join(', ')
var files: list<string> = globpath(dirs, '**/*.vim', v:true, v:true)
->sort((a: string, b: string): number => {
var [x, y] = map([a, b], (i, w) => fnamemodify(w, ':t')->tolower())
return x > y ? 1 : -1
})
export def OpenMenuWithConfigFiles()
var names: list<string> = mapnew(files, (i, v) =>
keys[i] .. ':' .. fnamemodify(v, ':t'))
popup_menu(names, {
filter: FilterQuickLink,
callback: ExitQuickLink,
})
enddef
def FilterQuickLink(id: number, key: string): bool
var i = match(keys, key)
if i > -1
popup_close(id, i + 1)
endif
return popup_filter_menu(id, key)
enddef
def ExitQuickLink(id: number, value: number)
if value > 0
silent execute 'e ' .. paths[value - 1]
endif
enddef
플러그인에서는 autoload/helper9.vim
파일을 autoload 타입으로 반입하고, 키맵이 눌렸을 때 호출하도록 한다. 참고로 키 맵의 동작이 함수 호출 하나 밖에 없는 경우 :<C-u>
나 <Cmd>
대신에 <ScriptCmd>
를 사용하면 <SID>
를 붙이지 않고 키 맵 스코프에서 함수를 호출할 수 있다.
vim9script
import autoload helper9.vim
nnoremap <leader>rd <ScriptCmd>helper9.OpenMenuWithConfigFiles()<CR>