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>

Read more

워드프레스에서 고스트로 이전

워드프레스에서 고스트로 이전

이 글을 쓰면서도 믿기 힘든 사실인데, 블로그라는 걸 처음 시작한지가 20년이 되었습니다. 이글루스에서 처음 시작했다가, SK컴즈가 인수한다고 발표함과 동시에 워드프레스로 플랫폼을 옮겼죠. 워드프레스오 옮긴 이후에는 호스팅 환경을 이리 저리 옮기긴 했지만 거의 18년 가까이 워드프레스를 사용해온 것 같습니다. 그 동안 워드프레스는 블로깅 툴에서 명실상부한 범용CMS로 발전했습니다. 사실 웬만한 홈페이지들은 이제

By sooop
띄어쓰기에 대한 생각

띄어쓰기에 대한 생각

업무 메일을 쓸 때 가장 많이 쓰는 말 중에 하나가 메일 말미에 ‘업무에 참고 부탁 드립니다.‘인데요, 어느 날부터 아웃룩에서 이 ‘부탁 드립니다’가 틀렸다고 맞춤법 지적을 하기 시작했습니다. 맞는 말은 ‘부탁드립니다’라고 붙여 쓰는 거라고. 사실 아래아한글 시절부터 이전의 MS워드까지, 워드프로세서들의 한국어 맞춤법 검사 실력은 거의 있으나 마나 한

By sooop

구글 포토에서 아이클라우드로 탈출한 후기

한 때 구글 포토가 백업 용량을 무제한으로 제공해 주겠다고해서, 구글 포토를 사용해서 사진을 백업해왔습니다. 물론 이 이야기의 결말은 저나 이 글을 읽고 있는 여러분이나 모두 알고 있습니다. 사실 AI에게 학습 시킬 이미지 데이터를 모으기 위한 것일 뿐이라거나 하는 이야기는 그 당시에도 있었습니다만, 에이 그래도 구글인데 용량은 넉넉하게 주겠지…하는 순진한

By sooop

Julia의 함수 사용팁

연산자의 함수적 표기 Julia의 연산자는 기본적으로 함수이며, 함수 호출 표기와 같은 방식으로 호출하는 것이 가능합니다. 또한 그 자체로 함수이기 때문에 filter(), map() 과 같이 함수를 인자로 받는 함수에도 연산자를 그대로 적용하는 것이 가능합니다. 특히 + 연산자는 sum() 함수와 같이 여러 인자를 받아 인자들의 합을 구할 수 있습니다. 2 + 3 # = 5 +(2,

By sooop