콘텐츠로 건너뛰기
Home » vim에서 설정파일을 메뉴로 열기

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

개인적으로 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: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>