콘텐츠로 건너뛰기
Home » Vim에서 팝업 사용하기

Vim에서 팝업 사용하기

vim8부터 popup 기능이 새롭게 도입되었다. 이제 vim script에서 팝업을 만들어서 대화 상자나 메뉴 혹은 그외의 UI들을 팝업 레이어로 만들어서 편집기 위로 띄울 수 있게 되었다. 특히 비동기로 외부 job을 실행하는 함수를 작성하는 경우에 그 결과를 사용자에게 알려주는 용도로 사용하기에 용이하다. 이 기능을 어떻게 사용하는지 살펴보도록 하자.

먼저 팝업의 종류와 구조를 살펴보자.

  1. 팝업을 만드는 것을 창분할과 동일하게 새로운 창을 생성하게 된다.
  2. 창이 생성될 때 별도의 버퍼가 생성된다. 팝업 창에서 표시되는 내용은 모두 해당 버퍼에 있다. 이 버퍼는 팝업용으로 설정되어 있으며, 일부 조작이 제한된다.
  3. 팝업 창 내에는 커서가 있지만 표시되지 않는다. 팝업이 표시되는 동안에도 원래 편집 버퍼 내에 커서가 그대로 보이게 된다.
  4. 특별한 케이스로, 팝업을 통해서 터미널을 여는 것이 가능하다. 터미널 팝업을 연 경우에는 항상 팝업이 포커스를 가지며, 다른 창으로 이동이 허락되지 않는다. 그외에 몇가지 일반 팝업과는 다른 점들이 있으며, 터미널이 종료되면 팝업이 닫힌다.

터미널 외에 몇 가지 옵션과 동작이 미리 정의된 팝업들이 있다.

  • 알림창 : 메시지를 표시하며, 3초 뒤에 자동으로 제거된다.
  • 대화상자: 메시지와 더불어 확인/취소 등 간단한 버튼을 제시한다.
  • 메뉴: 여러 항목 중에서 선택하게한다.

오늘은 간단한 팝업 예제를 몇가지 소개하겠다.

알림 메시지

알림메시지는 popup_notification() 함수를 사용해서 띄울 수 있다. 이 함수는 두 개의 인자를 갖는데, 첫번째 인자는 표시할 메시지이며, 두 번째 인자는 팝업의 옵션을 담고 있는 사전 타입의 객체이다. 두 번째 인자는 생략 불가능하며, 지정할 내용이 없는 경우 {}와 같이 빈 사전을 넘겨야 한다.

:let notewin = popup_notification('hello world!', {})

위 코드를 실행하면 “hello world!” 문구가 화면 상단 왼쪽에 레이어로 표시되는 것을 볼 수 있다. 알림 팝업을 호출할 때에는 "pos": "center" 옵션을 사용하면 알림 팝업이 화면 가운데에 표시되게 할 수 있다. 팝업을 생성하는 모든 편의 함수들은 popup_create()를 기초로 하는데, 사용가능한 옵션들 역시 해당 함수의 옵션을 그대로 사용한다. 팝업 생성에 관한 옵션은 :h popup_create-arguments 를 참고하자.

다이얼로그

팝업의 옵션에는 콜백 함수가 있다. 이 콜백함수가 지정된 팝업은 닫히는 시점에 콜백 함수를 호출하게 된다. 팝업의 콜백함수는 (winid, result)를 인자로 받으며, 이 때 result 는 다이얼로그에서 선택했던 버튼이나, 메뉴에서 골랐던 항목이 된다. 팝업에서 받는 키 입력은 필터함수에 의해 처리된다. vim에서 기본적으로 제공하는 필터함수는 popup_filter_yesno()와 popup_filter_menu()가 있다.

다이얼로그로 호출하는 경우에는 popup_filter_yesno()를 사용한다. 이 함수는 y, Y, n, N 키가 눌려지는 것을 감지하여 처리한다. 그 외에 Esc 및 x 키가 n 과 동일하게 작동하도록 되어 있다. 예제를 통해서 사용하는 방법을 알아보자. 먼저 다이얼로그 콜백 함수를 작성한다. 다이얼로그 콜백에서 result 값은 ‘0’, ‘1’ 중 하나의 값을 받는다. (yes = ‘1’ 이다.)

function! s:callback_dialog(wid, result) abort
  if a:result == 1
    " 대화상자에서 y를 입력한 경우
    echo 'You selected <YES>'
  else
    echo 'You selected <NO>'
  endif
endfunction
function! AskYN() abort
  let Callback = function('s:callback_dialog')
  call popup_dialog('Proceed? (Y/N)'), #{
          \filter: 'popup_filter_yesno',
          \callback: Callback,
  })
endfunction

filter 옵션에는 문자열로 필터 함수의 이름을 넘기는데, 함수 객체를 넘기는 것도 허용된다(어차피 표준 함수라서 그냥 이름을 넘기는 것이 글자수를 절약함).

메뉴 입력하기

단순히 Y/N 입력이 아닌 메뉴에서 선택하는 UI를 만들고자 할 때에는 popup_menu() 함수를 사용한다. 팝업 메뉴는 여러 선택지를 보여주고 그 중에 하나를 선택한다. 따라서 메뉴 팝업을 생성할 때 표시할 콘텐츠는 문자열의 리스트를 전달한다. 팝업이 표시되면 첫번째 항목이 하이라이트 되어 표시되고, 이 때 j/k 키를 사용하여 선택 옵션을 오가며, 엔터키로 입력을 결정한다. 또 Esc 키를 눌러서 입력을 취소할 수 있다. 팝업 메뉴의 입력처리는 기본적으로 popup_filter_menu() 함수를 사용한다.

" 메뉴 콜백
function s:my_menu_handler(winid, result) abort
  echo "you selected " . a:result . "."
endfunction
let items = "Apple Banana Cherry Oragne"->split()
let w = popup_menu(items, #{
        \pos: 'center',
        \filter: 'popup_filter_menu',
        \callback: function('s:my_menu_handler') })

위 코드를 실행하면 과일 이름으로 표시되는 메뉴가 표시된다. Apple이 하이라이트 되어 있고 j/k 키로 항목을 선택할 수 있다. 결정에는 Enter키를 사용한다. 항목을 선택하면 선택한 항목의 값이 출력되는데, 1부터 시작하는 항목의 인덱스가 표시되는 것을 확인할 수 있다. 만약 Esc키를 눌러 항목의 선택을 취소하면 result 값은 -1이 된다.

단축키로 메뉴 선택하기

메뉴 필터 함수를 커스텀함수로 대체하면 j/k로 사용하는 것 외에 단축키를 써서 선택하는 기능을 만들 수 있다. 예를 들어 위 예에서 a, b, c, o 키를 눌러서 각 이름에 해당하는 커스텀 필터는 다음과 같은 규격을 맞춰서 작성할 수 있다.

  1. winid와 key 값을 인자로 받는다.
  2. key 값이 특정한 키 인지를 파악한다.
  3. 메뉴에서 선택됐음을 확인하면 popup_close()를 호출하고 그 값을 그대로 리턴한다. 이 때 전달해야 하는 인자는 a:winid 와 메뉴에서 선택한 항목의 값이다.
  4. 만약 필터에서 처리할 수 없는 키라면 popup_filter_menu(a:winid, a:key)를 호출하고 그 값을 리턴한다. 이렇게하면 j/k 키 처리는 계속 유지된다.

다음은 위 예제에서 메뉴 필터를 따로 작성한 예이다. Apple, Banana, Cherry, Orange의 첫 글자를 입력하면 바로 메뉴가 닫히고 해당 항목을 선택했다는 알림 팝업을 표시한다.

function! s:my_menu_filter(id, key) abort
  let l:i = stridx('AaBbCcOo', a:key)
  if i >= 0
    return popup_close(a:id, i/2+1)
  endif
  return popup_filter_menu(a:id, key)
endfunction
let s:items = '[A]pple [B]anana [C]herry [O]range'->split()
function! s:my_menu_handler(winid, result) abort
  if result > 0
    call popup_notification(s:items[a:result], #{pos: 'center', time: 1})
  endif
endfunction
let w = popup_menu(s:items, #{
        \pos:'center',
        \filter: function('s:my_menu_filter'),
        \callback: function('s:my_menu_handler') })