Wireframe

vim에서 설정파일을 메뉴로 열기

개인적으로 vim 설정을 하나의 vimrc 파일에 몰아서 만들기 보다는 여러 개의 파일로 쪼개어 사용자 설정 폴더 내에 배치해두고 사용하고 있다. 그러다보니 특정한 설정을 수정하려 할 때, 해당 파일을 빨리 찾아서 여는 기능이 필요했다. 사용자 정의 Ex 명령으로 만드는 방법이 있지만, Vim8의 팝업 기능을 사용하면 메뉴에서 원하는 파일을 선택해서 여는 기능을 구현할 수 있다.

이 기능을 구현하는 코드를 vim9 스크립트 버전으로 변경하여 공개한다.실제 함수는 autoload 디렉토리 아래에 만들어서 최초 실행하는 시점에 로딩되도록 하고, 맵핑만 vimrc 혹은 별도의 플러그인 폴더 내의 스크립트에서 정의한다. vim-plug 로 설치된 플러그인 파일은 검색에서 제외하기 위해서 특정한 디렉토리들만 지정해서 탐색하도록 할 것이다. 파일을 검색하고 목록을 만드는 기능을 구현하기 위해 필요한 vimscript 함수들은 다음과 같다. 자세한 내용은 vim 도움말 참고.

파일 목록 만들기

globpath() 함수를 사용하여 특정한 디렉토리에서 '**/*.vim' 이라는 패턴으로 검색하면 해당 디렉토리 및 그 하위의 모든 디렉토리에서 확장자가 .vim인 모든 파일을 찾을 수 있다. 이 때 전달하는 경로에 여러 디렉토리를 동시에 전달할 수 있는데, 각각의 디렉토리를 콤마로 연결하여 전달하면 된다. 이 함수를 찾은 파일들의 경로를 문자열로 리턴하는데, 네 번째 인자를 v:true 로 전달하여 문자열의 리스트로 받는 것이 가능하다.

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
  

메뉴 호출하기

핸들러 함수들을 준비했으니, 메뉴를 호출하자. 이미 만들어놓은 fileskeys 를 조합해서 메뉴 항목을 만들고, 이를 콜백, 필터 함수 옵션과 함께 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>
Exit mobile version