tools/vim

Learn Vimscript The Hard Way - 51. Potion Section Movement - part two

seul chan 2020. 5. 6. 23:44

Potion Section Movement - part two

해당 장도 길이가 길기 때문에 두 파트로 나누어서 작성하였다.

Potion section movement - part one에서 이어지는 글이다.

Top Level Text Sections

첫 번 째 let pattern = '...'을 다음으로 변경해보자.

let pattern = '\v(\n\n^\S|%^)'

이 정규표현식의 동작을 이해하기 위해서 아래의 "section" 정의를 다시 기억해보자.

빈 줄 뒤에 오는 줄 (첫 글자가 띄어쓰기가 아닌)이나 파일의 첫줄

\v는 우리가 몇 번 다뤘던 "very magic" 모드이다.

나머지는 두 옵션으로 나눠진 정규표현식이다. 첫번째는 \n\n^\S인데, 이는 "빈 줄 뒤에 오는 줄, 뒤에는 non-whitespace 문자가 온다" 라는 뜻이다. 이는 위 정의의 첫번째와 같다.

다른 옵션은 %^이다. 이는 "파일의 시작"을 뜻하는 vim의 특별한 표현식이다.

이제 처음 두 매핑을 지정할 수 있게 되었다. 이를 저장하고 샘플 potion buffer (factorial.pn)에서 :set filetype=potion을 실행해보자. [[]]가 작동해햐나는데, 무언가 이상한 것을 볼 수 있다.

Search Flags

우리가 원하는 섹션이 아닌 그 위에 커서가 배치되어 있는 것을 볼 수 있다. 무슨일이 생긴 것인지 한 번 생각해보자.

정답은 /(or ?)을 통해 검색하면 커서가 검색 결과의 처음에 위치하기 때문이다. /foo를 검색하면 커서가 f에 위치하는 것과 같은 원리이다.

커서를 매칭의 마지막에 위치시키고 싶다면 search flag를 사용하면 된다. 다음과 같이 Potion 파일을 검색해보자.

/factorial/e

factorial을 찾아서 커서를 이동시킬 것이다. n을 눌러 몇 번 더 다음 매칭을 찾아보자. e 플래그로 인해 매칭의 마지막에 커서가 위치하는 것을 볼 수 있다.

이를 반대 방향으로도 실행시켜보자.

?factorial?e

이 또한 동일하게 동작한다. 이제 우리의 함수에 search flag를 추가해주자.

function! s:NextSection(type, backwards)
    if a:type == 1
        let pattern = '\v(\n\n^\S|%^)'
        let flags = 'e'
    elseif a:type == 2
        let pattern = 'two'
        let flags = ''
    endif

    if a:backwards
        let dir = '?'
    else
        let dir = '/'
    endif

    execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction

두 가지가 변경되었다. 먼저 우리는 각 section movement에 따라 flags 변수를 추가하였다. 우선은 첫 번째만 고려하여 e를 추가해주었다.

두 번 째로, 우리는 dirflagsexecute문에 추가하였다. 이는 실행문 뒤에 ?e/e를 추가하는 효과를 준다.

파일을 저장하고 다시 potion file에서 :set ft=potion을 실행시켜보자. ]][[를 실행시키면 원하는 대로 동작하는 것을 볼 수 있다!

Function Definitions

이제 두 번째 "section"으로 넘어갈 차례이다. 우리의 두 번째 section 정의를 떠올려보자.

첫 글자로 non-whitespace인 문자가 오고, 해당 줄에 =가 포함되고 줄의 마지막이 :으로 끝나는 경우

두 번 째 let pattern = '...'줄을 다음과 같이 변경해보자.

let pattern = '\v^\S.*\=.*:$'

이 정규표현식은 저번것보다 꽤 간단하다. 해석은 연습으로 남겨두겠다.

  • \v: very magic
  • ^\S: 줄의 맨 처음에 non-whitespace 문자가 와야함을 의미
  • .*: 모든 문자 0개 이상
  • \=: (escape 된) =
  • .*: 모든 문자 0개 이상
  • :$: 줄의 마지막에 :

이를 저장하고 factorial.pn에서 :set filetype=potion을 실행시켜 보자. 그리고 ][, []를 실행하여 예상했던 대로 작동하는지 살펴보자.

우리는 따로 search flag가 필요 없기 때문에 그냥 기본으로 커서가 가장 처음에 위치하게 그대로 두어도 된다.

Visual mode

우리의 section movement는 normal mode에서는 잘 동작하지만, visual mode에서도 작동하게 하려면 몇 가지 작업이 더 필요하다. 먼저 함수를 다음과 같이 수정해보자.

function! s:NextSection(type, backwards, visual)
    if a:visual
        normal! gv
    endif

    if a:type == 1
        let pattern = '\v(\n\n^\S|%^)' 
        let flags = 'e'
    elseif a:type == 2
        let pattern = '\v^\S.*\=.*:$'
        let flags = ''
    endif

    if a:backwards
        let dir = '?'
    else
        let dir = '/'
    endif

    execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction

두 가지가 변경되었다. 첫 번째는 함수가 visual이라는 추가적인 인자를 받아 함수가 visual mode에서 실행되었는지 아닌지를 확인할 수 있게 하였다. 두 번째는 visual mode일 경우 gv를 실행하여 visual selection을 복구할 수 있게 하였다.

이것이 왜 필요할까? 이를 확실하게 해 보자. 버퍼의 아무 텍스트나 visual mode로 선택한 후 다음 명령어를 실행시켜보자.

:echom "hello"
hello

hello가 출력되지만 visual selection도 사라져 버린다!

:로 명령어 모드를 실행시키면 visual selection은 항상 초기화된다. gv 명령어는 이전 visual selection을 다시 선택한다. 이는 일상적인 작업에서도 많이 사용할 수 있는 매우 유용한 명령어이다!

이제 기존 매핑에 새로 추가한 visual 인자를 위해 0을 추가해주자.

noremap <script> <buffer> <silent> ]]
        \ :call <SID>NextSection(1, 0, 0)<cr>

noremap <script> <buffer> <silent> [[
        \ :call <SID>NextSection(1, 1, 0)<cr>

noremap <script> <buffer> <silent> ][
        \ :call <SID>NextSection(2, 0, 0)<cr>

noremap <script> <buffer> <silent> []
        \ :call <SID>NextSection(2, 1, 0)<cr>

복잡할 것이 없다. NextSection 함수의 마지막에 0 (visual mode가 아님)을 추가하였 을 뿐이다. 이제 visual mode 매핑도 추가해보자.

vnoremap <script> <buffer> <silent> ]]
        \ :<c-u>call <SID>NextSection(1, 0, 1)<cr>

vnoremap <script> <buffer> <silent> [[
        \ :<c-u>call <SID>NextSection(1, 1, 1)<cr>

vnoremap <script> <buffer> <silent> ][
        \ :<c-u>call <SID>NextSection(2, 0, 1)<cr>

vnoremap <script> <buffer> <silent> []
        \ :<c-u>call <SID>NextSection(2, 1, 1)<cr>

위 매핑들은 visual 인자를 모두 1로 추가하여 vim이 section movement를 실행한 이후에도 이전 selection을 유지할 수 있게 하였다. 뿐만 아니라 이전 장인 Grep Operator 챕터에서 배운 <c-u> 트릭도 사용하였다.

파일을 저장하고, potion 파일에서 :set ft=potion을 실행시켜보자. 이제 새로 추가된 매핑을 시도해보자. v]]d[]가 잘 작동할 것이다.

Why Bother?

아주 작은 함수에 긴 챕터를 할애했지만, 여기서 여러 유용한 것들을 배웠을 것이다.

  • nnoremap대신 noremap을 사용하여 movement와 motion에 사용할 수 있는 mapping을 만들었다.
  • 매핑을 간소화시킬 수 있는 여러 인자를 받는 한 가지 함수를 만들었다.
  • execute 'normal! ...'을 만들었다.
  • 정규표현식을 사용한 search를 활용하였다.
  • %^(begging of the file)같은 특별한 정규표현식을 사용하였다.
  • 검색 방식을 수정하는 search flag를 사용하였다.
  • visual selection을 되살려 visual mode 매핑을 활용하였다.

exercises를 마저 하고 (몇개의 :help만 읽으면 된다!) 아이스크림을 먹어라! 이 챕터를 끝마치면 그럴 가치가 있다.

Exercises

  • Read :help search(). This is a useful function to know, but you can also use the flags listed with the / and ? commands.
flag lists

'b'    search Backward instead of forward
'c'    accept a match at the Cursor position
'e'    move to the End of the match
'n'    do Not move the cursor
'p'    return number of matching sub-Pattern (see below)
's'    Set the ' mark at the previous location of the cursor
'w'    Wrap around the end of the file
'W'    don't Wrap around the end of the file
'z'    start searching at the cursor column instead of zero
  • Read :help ordinary-atom to learn about more interesting things you can use in search patterns.
      ordinary atom ~
      magic   nomagic    matches ~
|/^|    ^    ^    start-of-line (at start of pattern) |/zero-width|
|/\^|    \^    \^    literal '^'
|/\_^|    \_^    \_^    start-of-line (used anywhere) |/zero-width|
|/$|    $    $    end-of-line (at end of pattern) |/zero-width|
|/\$|    \$    \$    literal '$'
|/\_$|    \_$    \_$    end-of-line (used anywhere) |/zero-width|
|/.|    .    \.    any single character (not an end-of-line)
|/\_.|    \_.    \_.    any single character or end-of-line
|/\<|    \<    \<    beginning of a word |/zero-width|
|/\>|    \>    \>    end of a word |/zero-width|
|/\zs|    \zs    \zs    anything, sets start of match
|/\ze|    \ze    \ze    anything, sets end of match
|/\%^|    \%^    \%^    beginning of file |/zero-width|        *E71*
|/\%$|    \%$    \%$    end of file |/zero-width|
|/\%V|    \%V    \%V    inside Visual area |/zero-width|
|/\%#|    \%#    \%#    cursor position |/zero-width|
|/\%'m|    \%'m    \%'m    mark m position |/zero-width|
|/\%l|    \%23l    \%23l    in line 23 |/zero-width|
|/\%c|    \%23c    \%23c    in column 23 |/zero-width|
|/\%v|    \%23v    \%23v    in virtual column 23 |/zero-width|