Wireframe

자동완성 커스텀하기 (vim)

사실 vim은 별다른 설정이 없어도 입력모드에서 <C-X> 로 시작하는 몇 가지 키 시퀀스를 누르면 현재 버퍼 혹은 열려 있는 버퍼에서 단어, 라인 등의 내용을 자동으로 완성하는 기능을 제공한다. 그 외에도 ctags 를 사용하여 코드에서 추출한 키워드와 태그를 자동완성할 수도 있다. 그 외에 이제까지 입력한 적이 없는 내용에 대해서도 몇 가지 자동완성과 관련된 옵션을 설정하면 입력 모드에서 자동 완성을 확장할 수 있다.


'completefunc' , 'omnifunc' 같은 옵션을 설정하면 커스텀 자동 완성을 사용할 수 있다. 이 옵션을 설정할 때에는 별도의 사용자 정의 함수 이름을 옵션 값으로 설정한다.

'completefunc' 옵션의 경우 입력 모드의 <C-X><C-U> 기능을 통해 자동 완성 목록을 호출하게 된다. 'omnifunc' 옵션의 경우에는 <C-X><C-O> 를 사용하여 호출한다. 이 기능은 각 옵션에서 설정된 함수를 사용하여 현재 커서 위치에서 사용할 수 있는 자동완성 가능한 단어의 목록으로부터 단어를 완성한다. 이 때 이 후보들을 생성하는 것이 해당 옵션에서 설정한 함수들인 셈이다.

'completefunc''omnifunc' 옵션은 실질적으로 완전히 동일한 기능이라고 볼 수 있다. 두 옵션의 차이는 completefunc 옵션은 사용자가 원하는 조건이나 상황에서의 자동완성을 설정하는 기능인 것에 비해, omnifunc 옵션은 주로 파일 타입별로 설정하여 특정한 파일 타입 내에서 통용되는 자동완성 기능에 대한 설정으로 주로 쓰인다는 것이다. vim 설치 폴더 내의 autoload 폴더에는 타입별 자동완성 관련된 vim 스크립트 파일이 기본적으로 함께 설치되어 있고, 여기서 omnifunc 옵션에 사용되는 함수들을 정의하고 설정하고 있다.

사용자 정의 자동완성 함수 구현하기

직접 자동 완성 함수를 구현하는 방법을 알아보자. 먼저 이 기능이 작동하는 시나리오를 이해할 필요가 있다. 입력 중에 자동 완성 목록을 호출하는 키가 입력될 때, 실제로 vim은 해당 함수를 두 번 호출한다. 첫번째 호출에서 자동 완성 함수는 자동 완성할 단어가 대체할 범위를 결정해준다. 이 범위는 주로 현재 입력 중인 단어, 그러니까 커서로부터 앞에 위치한 가장 가까운 공백 이후의 위치를 리턴한다. 자동 완성 단어가 대체할 범위를 리턴 받았다면 vim은 다시 한 번 같은 함수를 호출하여 해당 위치에 들어갈 단어들의 목록을 리턴받는다.

따라서 설정에 필요한 자동완성 함수는 (findstart, base) 의 두 개의 인자를 받는다. 첫번째 인자는 이 호출이 첫번째 호출인지 두 번째 호출인지를 결정한다. 두 번째 인자는 커서가 위치한 바로 앞까지의 해당 라인의 문자열이 전달된다.

첫번재 호출에서 리턴해야 하는 값은 0과 col('.') 사이의 값을 리턴한다. 이 때 정해준 값과 현재 커서 위치의 컬럼 사이의 내용이 선택한 자동완성 후보로 교체될 것이다. 커서 위치보다 더 큰 값을 리턴하면 무조건 커서 위치를 사용하게 된다.

첫번째 호출에서 음수를 리턴하면 자동 완성을 사용할 수 없는 것으로 판단하게 된다. -2를 리턴하면 자동 완성을 취소하지만 자동 완성 모드를 떠나지 않는 것으로 작동하며, -3을 리턴하면 자동 완성을 취소하면서 자동 완성 모드를 떠나게 된다.

두 번째 호출될 때 vim은 첫번째 호출에서 얻은 위치로부터 현재 커서 위치까지 입력된 문자열을 두번째 인자로 전달한다. 이제 두 번째 호출에서는 이 문자열을 사용하여 대응할 자동완성 후보의 목록을 생성하여 리턴하면 된다. 만약

두 번째 호출에서는 실제로 매치할 단어들의 목록을 리턴한다. 보통의 구현에서는 이 단어들은 각각 a:base 인자를 포함하는 경우가 많다. 만약 매치하는 후보가 없다면 빈 목록을 리턴한다.

간단한 예제를 만들어서 구현해보도록 하자. 다음 예제는 각 월의 약어를 자동으로 완성하는 자동 완성 함수를 구현하고, 'completefunc' 옵션으로 설정한 것이다.

" complete_example.vim
function s:complete_month(findstart, base)
  if a:findstart == 1
    let x = col('.')
    while x > 0 && getline('.')[x - 1] !~ '\s'
      let x = x - 1
    endwhile
    return x
  endif
  " 두 번째 호출
  let months = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'->split()
  return filter(months, {i,e -> e ~=# a:base})
endfunction

setlocal completefunc=s:complete_month  

파일을 저장한 후에 해당 스크립트를 :source % 명령으로 로드하면 커스텀 자동완성이 작동한다. 입력모드에서 ‘m’을 입력한 다음, <C-X><C-U>를 순서대로 누르면 “MAR”, “MAY”가 자동완성 목록에 뜨는 것을 확인할 수 있다.

설정값으로 함수를 설정하는 방법

'completefunc''omnifunc' 외에도 vim에는 함수를 설정값으로 사용하는 설정항목들이 있다. 이러한 설정 항목을 세팅할 때에는 함수의 이름이나, FuncRef 객체, 함수의 참조를 그 값으로 설정할 수 있다. 람다식도 함수 객체의 일종이기 때문에 설정이 가능한데, 공백을 옵션의 구분으로 인식하기 때문에 이스케이프해야 한다는 점을 주의하면 된다.

set opfunc=MyOpFunc   "함수의 이름
set opfunc=function("MyOpFunc")
set opfunc=funcref("MyOpFunc")
set opfunc={a\ ->\ MyOpfunc(a)}

"로컬 함수
set opfunc=s:MyLocalFunc
set opfunc=<SID>MyLocalFunc

"옵션 변수를 사용하여 설정
let Fn = function('MyFunc')
let &tagfunc = Fn

자동 완성과 관련된 오토커맨드

만약 자동완성이 끝나는 시점에 특정한 액션을 수행하고 싶다면 CompleteDonePre, CompleteDone 이벤트에 대해 autocmd를 설정해놓을 수 있다. 해당 액션에서 v:completed_item 을 사용하면 자동완성에서 선택된 항목에 대한 정보를 얻을 수 있다. 이 정보는 사전 형식으로 다음과 같은 몇 가지 정보를 포함한다.

{'word': 선택한 단어,
 'abbr': 단어가 긴 경우 메뉴에서 표시되는 단축이름
 'menu': 메뉴에 표시되는 단어(word/abbr)뒤에 추가 표시 정보
 'info': 별도의 팝업창에서 표시할 정보
 'kind': 선택한 단어의 종류(변수, 함수, 클래스 등)
}

# 더 많은 정보는 :h complete-items 를 확인해보자.

참고로 자동완성 함수의 리턴값은 단어의 리스트가 아닌, 이 자동완성 항목 사전의 리스트로 리턴할 수도 있다.

강제로 자동완성 호출하기

내장함수 complete() 를 사용하면, 현재 라인의 주어진 위치로부터 즉시 자동완성 메뉴를 호출할 수 있다. 이 때 메뉴에 표시되는 후보 단어의 항목은 직접 전달된다. vim 도움말에 있는 예제를 살펴보자.

func! ListMonths()
  call complete(col('.'), [
    \ 'January', 'February', 'March', ...
    \ ]
  return ''
endfunc

inoremap <F5> <C-R>=ListMonths()<CR>

위 스크립트를 로드한 후 입력모드에서 <F5> 키를 누르면 앞서 입력한 단어에 무관하게 각 월의 목록이 팝업 메뉴로 표시된다. 이 기능은 그 자체로는 별 쓸모가 없지만 complete() 함수가 어떤 식으로 작동하는지를 보여준다. 이 함수는 (자동완성 단어로 교체할 위치, 단어의 목록)을 인자로 받아서, 즉시 해당 위치에 대해 자동완성 목록을 호출하는 기능을 수행한다.

complete() 함수의 괴상한 점은 -> 연산자를 통한 메소드식 호출에서 단어의 목록으로부터 연쇄가 가능하다는 것이다. 멧소드 호출은 항상 왼쪽식의 평가값을 오른쪽 함수의 첫번째 인자로 넣는다는 점에서 이상하게 작동한다. 왜냐하면 이 함수는 단어 목록을 첫번째 인자로 전달한 경우, 정상적으로 작동하지 않기 때문이다. 어쨌든, 특정한 상황에서 'completefunc', 'omnifunc' 옵션에 의존하지 않고 자동완성을 적용하려는 아이디어가 있다면 사용할 수 있을 것 같다. 외부에서 후보 단어를 얻은 후 다음과 같은 식으로 전달해서 자동완성을 즉시 사용할 수 있다.

call GetMatches()->complete(col('.'))

참고로 <expr> 키 맵에서는 이 기능을 사용할 수 없다. (표현식 평가 중에는 버퍼를 수정할 수 없기 때문)

커스텀 명령의 자동 완성

약간 논외의 내용인데, 커스텀 명령을 정의할 때에도, 인자에 대한 자동 완성을 커스텀 함수로부터 지정할 수 있다. -complete= 플래그를 사용하는데 이 때 -complete=custom,함수이름 을 쓰거나 -complete=customList,함수이름 을 쓸 수 있다. 전자의 경우, 함수는 문자열을 리턴하며, vim은 이 문자열의 각 행을 자동완성 후보로 사용하고, 입력된 내용을 기반으로 자동으로 매치해준다. 후자의 경우에는 문자열의 리스트를 사용하며, 매치 필터링은 함수 내에서 처리되어야 한다.

아래 함수는 커스텀 명령에서 인자에 자동완성을 구현하는 예시이다. custom 을 사용하는 경우에는 필터링을 vim이 알아서해준다.

function! s:complete_month(lead, cmdline, curpos)
  return "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"->split()
        \->join("\n")
endfunction

command! -nargs=1 -complete=custom,s:complete_month Foo echom <q-args>

vim9script 버전

앞 페이지와 동일한 코드인데 vim9script 버전으로 작성한 것이다.

vim9script

def CompleteMonths(findstart: number, base: string): any
  if findstart == 1
    var s = col('.')
    while s > 0 && getline('.')[s - 1] !~ '\s'
      s = s - 1
    endwhile
    return s
  endif
  var months = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'->split()
  return filter(months, (i, e) => e =~# base)
enddef

setlocal completefunc=CompleteMonths
Exit mobile version