tools/vim

Learn Vimscript The Hard Way - 33. Case Study: Grep Operator, Part Two

seul chan 2020. 4. 12. 22:09

Case Study: Grep Operator, Part Two

앞 장에서는 기본적인 해결책을 작성해 보았다. 이제는 그 명령어에 살을 붙여보자.

우리의 원래 목표는 "grep operator"를 만드는 것이다. 새로운 것들을 많이 다루어야 하겠지만, 우선 저번 장에서 했던 방식으로 진행해보자: 간단하게 시작해서 필요한 기능으로 바꾸기

이전 장에서 사용한 매핑 (<leader>g)을 동일하게 사용할 것이기 때문에 이전 장에서 ~/.vimrc에 추가한 매핑을 주석처리 해주자.

vim 파일에서 주석은 줄 앞에 "를 추가하면 된다.

" nnoremap <leader>g :silent execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>

operator를 만드는 것은 여러 코멘드를 쉽게 작성하게 해준다. 이를 ~/.vimrc에 추가할 수도 있지만, 이번에는 분리된 파일로 만들어보자.

우선, vim plugin 디렉토리를 찾아야 한다. 맥이나 Linux는 ~/.vim/plugin에 있을 것이고, 윈도우라면 vimfiles 디렉토리에 있을것이다.

vim이 어디에 있는지 궁금하다면 :echo $HOME 명령어를 실행시키면 알 수 있다. 보통은 사용자 디렉토리에 있을것이다.
Plug같은 플러그인 매니저를 사용중이라면 plugged, bundle같은 해당 디렉토리로 들어가면 된다.

Skkeleton

Vim operator는 두가지 컴포넌트로 구성된다 - 함수와 매핑이다. 우선 다음 명령어를 grep-opeerator.vim 파일에 추가해보자.

nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@

function! GrepOperator(type)
    echom "Test"
endfunction

해당 파일을 다 작성하고 :source %로 적용시켜보자. <leader>giw와 같은 명령어 (grep inside word)를 실행시키면 vim은 iw 모션을 받은 이후 Test를 출력할 것이다.

위 예시에서 함수 자체는 새로운 내용이 없지만, 매핑은 조금 더 복잡하다.

우선 우리는 operatorfunc에 우리가 만든 함수를 set한 뒤, g@로 해당 함수를 opeerator로 호출하였다.

일단은 해당 내용을 magic이라고 생각하고 이후 이 내용을 더 깊게 살펴볼 것이다.

Visual Mode

위 예시는 operator를 normal mode에서 추가하였다. (nnoremap) 이를 visual mode에도 추가해보자. 아래 명령어를 아까 만든 파일 (grep-operator.vim)의 맨 윗줄에 추가해주자.

vnoremap <ledaer>g :<c-u>call GrepOperator(visualmode())<cr>

해당 파일을 적용해주고 (:source %) visual mode로 아무 텍스트를 선택한 후 <leader>g를 사용해보자. vim이 Test를 출력하는 것을 볼 수 있다.

<c-u>는 우리가 지금까지 보지 못한 새로운 매핑이다. 아무 텍스트를 선택한 후 :를 눌러보자. :를 눌렀을 뿐인데 command line에 :'<,'>가 자동으로 써진 것을 볼 수 있을것이다.

vim은 vimsual mode에서 선택된 범위를 코멘드에서 사용할 수 있게 제공해준다 ('<,'>) 하지만 위의 예시에서는 선택된 텍스트가 중요하지 않으므로 <c-u>를 눌러서 "커서로부터 첫줄까지를 제거" 해준다. 이는 :만 남겨두고, call 명령을 실행시킬 수 있게 해준다.

위 설명이 헷갈리다면, :로 명령어 모드로 진입한 후 아무 명령어나 입력하고 <c-u>를 눌러보자. 작성한 명령어가 모두 제거된 것을 확인할 수 있다.

call GrepOperator()는 단순히 함수를 호출한 것이지만 함수의 인자로 넣은 visualmode()는 처음 보는 것이다. 이는 vim의 빌트인 함수로 visual mode에서 가장 마지막에 선택한 항목에 따라 "v""V", Ctrl-v를 반환한다.

문자열을 선택한 경우 "v", 라인을 선택한 경우 "V", 문단을 선택한 경우 Ctrl-v를 반환한다. visual mode로 문자, 라인, 문단을 각각 선택해보고 (각각 v, V, Ctrl-v로 선택 가능하다) :echo visualmode()를 실행시켜보면 확실하게 확인 가능하다.

Motion Types

우리가 작성한 GrepOperator 함수는 type 인자를 받는다. visual mode에서는 visualmode() 함수로 반환된 내용을 type으로 넘기는 것을 알 수 있었다. 하지만 normal mode에서 operator를 사용하면ㅁ 어떻게 type을 넘길 수 있을까?

파일의 내용을 아래와 같이 수정해보자. echom "Test" 대신 echom a:type으로 인자로 받은 type을 그대로 출력하게 변경하였다.

nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

function! GrepOperator(type)
    echom a:type
endfunction

:source %를 실행시키고 여러가지 방법으로 테스트를 해보자. 다음은 예시

  • viw<leader>gv를 출력: characterwise visual mode
  • Vjj<leader>gV를 출력: linewise visual mode
  • <leader>giwchar를 출력: used characterwise motion with the operator
  • <leader>gGline을 출력: used linewise mohion with the operator
    한다.

Copying the Text

함수에서는 user가 검색하고자 하는 text를 가져와야 하는데, 가장 쉬운 방법은 이를 복사하는 방법이다. 함수를 아래와 같이 수정해보자.

nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

function! GrepOperator(type)
    if a:type ==# 'v'
        execute "normal! `<v`>y"
    elseif a:type ==# 'char'
        execute "normal! `[v`]y"
    else
        return
    endif

    echom @@
endfunction

새로운 내용이 많이 추가되었다. :source %로 적용시킨 후에 여러가지 명령어를 테스트 해 보자. <leader>giw, <leader>g2e, (괄호 안에 커서를 두고) vi(<leader>g 등... 명령어를 실행시킬 때마다 해당 motion에 해당하는 텍스트를 출력하는 것을 볼 수 있다.

그럼 새로운 코드를 하나씩 분석해보자. 먼저 if문에서 a:type 인자를 체크한다. 만약 타입이 v라면 우리가 문자열 visual mode를 사용(v)한 것으로 visual mode에서 선택된 텍스트를 복사한다.

==# (case-sensitive 비교)를 사용한 것을 주목하자. 일반 비교 ==를 사용하면 ignorecase 옵션을 가진 유저가 V를 입력해도 v와 동일하게 처리될 것이다. 방어적으로 코딩하자!

두 번 째 케이스는 noraml mode에서 characterwise motion (위 예시에서 iw, 2e 등)을 사용하는 경우이다.

마지막 케이스는 단순히 리턴만 한다. linewise(V)/blockwise(Ctrl-v)의 경우는 무시하게 되는데, 그 이유는 grep이 기본적으로 라인을 넘나들지 않기 때문이다.

두 가지 if 케이스 모두 normal! 명령어를 실행시킨다.

  • 우리가 원하는 텍스트 범위를 비주얼로 선택
    • mark를 선택된 범위의 가장 앞으로 이동 (<)
    • characterwise visual mode 진입 (v)
    • mark를 선택된 범위의 가장 끝으로 이동 (>)
  • 선택된 text를 복사

mark에 대해서는 지금 정확히 알 필요 없다. 이 챕터의 마지막에서 제대로 배우게 될 것.

마지막 라인ㄴ의 @@는 "unnamed" 레지스터를 얘기한다. (마지막으로 복사하거나 지운)

Escaping the Search Term

이전 장에서 사용한 것처럼 string escape 를 적용해보자.

nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

function! GrepOperator(type)
    if a:type ==# 'v'
        execute "normal! `<v`>y"
    elseif a:type ==# 'char'
        execute "normal! `[v`]y"
    else
        return
    endif

    echom shellescape(@@)
endfunction

special character (' 등)가 포함된 text를 visual mode로 선택한 뒤 <leader>g를 실행시켜보자. 해당 텍스트가 escaped 된 것을 볼 수 있다.

Running Grep

이제 실제 검색을 위해 grep! 명령어를 추가할 준비가 되었다. echom 줄을 다음과 같이 수정해보자.

nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

function! GrepOperator(type)
    if a:type ==# 'v'
        execute "normal! `<v`>y"
    elseif a:type ==# 'char'
        execute "normal! `[v`]y"
    else
        return
    endif

    silent execute "grep! -R " . shellescape(@@) . " ."
    copen
endfunction

저번 장에서 사용하였던 silent execute "grep! ..." 명령어를 실행시켰다.

:source %를 한 뒤 명령어를 실행시켜보자.

완전히 새로운 vim operator를 추가하였기 때문에 다양한 방식으로 사용이 가능하다.

  • viw<leader>g: word를 비주얼 모드로 선택한 후 grep
  • <leader>g4w: 앞의 네 word를 grep
  • <leader>gt;: ; 전까지 grep
  • <leader>gi[: 대괄호 안의 텍스트를 grep

Exercises

  • Read :help visualmode()

  • Read :help c_ctrl-u

  • Read :help operatorfunc

  • Read :help map-operator

help text에 나온 space를 세는 예시

nmap <leader>cs :set operatorfunc=CountSpace<CR>g@
vmap <leader>cs :<c-u>call CountSpace(visualmode(), 1)<CR>

function! CountSpace(type, ...)
    let sel_save = &selection
    let &selection = "inclusive"
    let reg_save = @@

    if a:0 " Invoked from visual mode, use gv command."
        silent execute "normal! gvy"
    elseif a:type == 'line'
        silent execute "normal! '[V']y"
    else
        silent execute "normal! `[v`]y"
    endif

    echomsg strlen(substitute(@@, '[^ ]', '', 'g'))

    let &selection = sel_save
    let @@ = reg_save
endfunction