tools/vim

Learn Vimscript The Hard Way - 49. Advanced Folding - part 3

seul chan 2020. 5. 3. 23:14

Advanced Folding - part 3

해당 장은 매우 길기 때문에 시리즈로 작성할 예정이다.

  1. Advanced folding - part 1
  2. Advanced folding - part 2
  3. Advanced folding - part 3

Finishing the Fold Function

꽤 긴 챕터였으므로, 이제 슬슬 folding 함수를 마무리지어보자. GetPotionFold 함수를 다음과 같이 수정해보라.

function! GetPotionFold(lnum)
    if getline(a:lnum) =~? '\v^\s*$'
        return '-1'
    endif

    let this_indent = IndentLevel(a:lnum)
    let next_indent = IndentLevel(NextNonBlankLine(a:lnum))

    if next_indent == this_indent
        return this_indent
    elseif next_indent < this_indent
        return this_indent
    elseif next_indent > this_indent
        return '>' . next_indent
    endif
endfunction

꽤 많은 새로운 코드가 추가되었다. 어떻게 작동하는지 하나씩 살펴보자.

Blanks

먼저 우리는 blank line을 체크한다. 이전 코드와 변한 것이 없기 때문에 이전과 동일하게 동작할 것이다.

Finding Indentation Levels

다음으로 우리는 이전 장에서 만든 두 개의 헬퍼 함수를 사용하여 현재 줄과 다음 non-blank 줄의 indent level을 계산하였다.

NextNonBlankLine이 에러 컨디션에서 -2를 반환하게 한 것을 기억할 것이다. 만약 이런 일이 발생하면 indent(-2)가 실행된다. indent() 함수가 존재하지 않는 줄을 받으면 -1을 반환하게 된다. :echom indent(-2)로 직접 실행해보자.

-1를 0보다 큰 어떤 shiftwidth로 나누든 간에 0을 반환한다. 이는 문제가 되어 보일 수 있지만 별 일 없다. 일단은 이것에 대해 걱정하지 말자. (:echom -1 / 2, :echom -1 / 4 모두 0을 반환한다)

Equal Indents

이제 우리는 현재 줄과 다음 non-blank 줄의 indentation level을 구했다. 이제 이들을 비교하여 현재 줄을 어떻게 fold할 것인지 결정할 수 있다.

위에서 사용한 if문이다.

if next_indent == this_indent
    return this_indent
elseif next_indent < this_indent
    return this_indent
elseif next_indent > this_indent
    return '>' . next_indent
endif

먼저 우리는 두 줄이 같은 indentation level을 가지고 있는지 확인한다. 만약 같다면, 간한히 해당 indentation level을 foldlevel로 주면 된다.

알기쉽게 다음 예시를 사용해보자.

a
b
    c
    d
e

c를 포함하는 세 번째 줄을 보면 indentation level은 1이다. 이는 다음으로 오는 non-blank 라인 ("d")과 같기 때문에 foldlevel로 현재 indentation level인 1을 반환하게 된다.

이와 마찬가지로 "a"줄 또한 다음 줄인 "b"와 indentation level이 같기 때문에 현재 indentation level인 0을 반환한다.

지금까지 살펴본 두 가지의 foldlevel을 채워보면 다음과 같다.

a       0
b       ?
    c   1
    d   ?
e       ?

우리의 마지막 "error" case도 처리된다! 헬퍼 함수가 -2를 반환하면 next_indent0이 되기 때문이다.

마지막 "e" 줄의 indent level이 0이기 때문에 next_indent또한 0이 될 것이다. 그래서 이 경우에는 0을 반환하게 된다.

a       0
b       ?
    c   1
    d   ?
e       0

Lesser Indent Levels

다시한번 우리의 if문은 다음과 같다.

if next_indent == this_indent
    return this_indent
elseif next_indent < this_indent
    return this_indent
elseif next_indent > this_indent
    return '>' . next_indent
endif

if문의 두 번째 부분은 현재 줄의 indent level보다 다음 줄의 indent level이 더 작은지 확인한다. 위 예시의 경우에는 "d" 줄이 해당될 것이다.

이런 케이스에서는 다시한번 현재 줄의 indentation level을 반환한다.

따라서 우리의 예시의 foldlevel은 다음과 같이 된다.

a       0
b       ?
    c   1
    d   1
e       0

물론 이 두 케이스를 ||를 사용하여 묶을 수 있겠지만 나(저자)는 명시적으로 분리시키는 것을 좋아한다.

또다시, 순전히 운으로, 이 케이스에서 다른 가능한 "error" 케이스가 해결된다. 다음과 같은 파일이 있다고 생각해보자.

a
    b
    c

if 문의 첫 번째 케이스는 "b" 줄을 해결해준다.

a       ?
    b   1
    c   ?

마지막의 "c" 줄은 indent level이 1이고 헬퍼 함수에 의해 next_indent0으로 세팅될 것이다. 땨랴서 두 번째 케이스를 따라 this_indent가 반환되고, foldlevel은 1이 된다.

a       ?
    b   1
    c   1

이는 "b"와 "c"가 함께 fold되기 때문에 잘 작동한 것이다.

Greater Indentation Levels

if문의 마지막은 조금 tricky하다.

if next_indent == this_indent
    return this_indent
elseif next_indent < this_indent
    return this_indent
elseif next_indent > this_indent
    return '>' . next_indent
endif

그리고 지금까지 사용한 우리의 예시는 다음과 같다.

a       0
b       ?
    c   1
    d   1
e       0

아직까지 우리가 발견하지 못한 줄은 "b"이다. 왜냐하면

  • "b"는 indent level이 0이다.
  • "c"는 indent level이 1이다.
  • 1은 0과 같지도 않고, 1은 0보다 작지도 않다.

if문의 마지막 부분은 다음 줄이 현재 줄보다 더 큰 indent level을 가지고 있는지를 확인한다.

이 상황에서 vim의 indent folding 방식이 잘못 작동했기 때문에 우리가 custom folding을 작성하게 된 이유이다.

indent folding method에서는 위 줄을 fold하지 않지만 우리는 fold하고 싶어서 custom method를 작성하기 시작했다.

다음 줄의 indent가 현재 줄보다 크다면, >문자를 포함한 다음 줄의 indentation level을 반환하게 된다. 대체 이것이 무엇일까?

fold 표현식에서 >1과 같은 string을 반환하는 것은 또 다른 vim의 "special" foldlevel이다. 이는 vim에게 현재 줄은 주어진 숫자의 fold level일때 열린다고 말해주는 것이다.

간단한 예시에서는 그냥 숫자를 반환해도 되지만, 우리는 이것이 왜 중요한지 곧 살펴볼 것이다.

이 예시에서 "b"는 fold level이 1일때 열리기 때문에 우리 예시는 다음과 같아진다.

a       0
b       >1
    c   1
    d   1
e       0

우리가 원했던 결과와 같다! 만세!

Review

여기까지 잘 만들었다면 당신을 자랑스럽게 여겨도 된다.

끝내기 전에, 우리의 factorial.pn으로 가서 fold 표현식이 어떻게 동작하는지 살펴보자.

factorial = (n):
    total = 1
    n to 1 (i):
        # Multiply the running total.
        total *= i.
    total.

10 times (i):
    i string print
    '! is: ' print
    factorial (i) string print
    "\n" print.

먼저 모든 blank line의 foldlevel은 undefined로 세팅된다.

factorial = (n):
    total = 1
    n to 1 (i):
        # Multiply the running total.
        total *= i.
    total.
                                         undefined
10 times (i):
    i string print
    '! is: ' print
    factorial (i) string print
    "\n" print.

그리고 다음 줄의 indentation level이 같은 경우에는 자신의 indentation level을 foldlevel로 가진다.

factorial = (n):
    total = 1                            1
    n to 1 (i):
        # Multiply the running total.    2
        total *= i.
    total.
                                         undefined
10 times (i):
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.

다음 줄의 indentation level이 현재 줄의 indentation level보다 작은 경우에도 동일한 상황이 발생한다.

factorial = (n):
    total = 1                            1
    n to 1 (i):
        # Multiply the running total.    2
        total *= i.                      2
    total.                               1
                                         undefined
10 times (i):
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.  

마지막 케이스는 다음 줄의 indentation level이 현재 줄의 indentation level보다 큰 경우이다. 이 때는 다음 줄의 foldlevel일 때 열리게 foldlevel이 세팅된다.

factorial = (n):                         >1
    total = 1                            1
    n to 1 (i):                          >2
        # Multiply the running total.    2
        total *= i.                      2
    total.                               1
                                         undefined
10 times (i):                            >1
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.     

이제 전체 파일에 대한 foldlevel을 구했다. 마지막으로 vim이 undefined 줄들에 대한 처리가 남았다.

앞서 undefined 줄은 근처 foldlevel에서 가장 작은 것을 따른다로 하였다.

이것이 vim이 설명하는 것이지만, 완전 정확하지는 않다. 이런 경우라면 우리의 undefined 줄은 위와 아래줄의 foldlevel이 모두 1이기 때문에 1의 foldlevel을 가질 것이다.

실제로는, blank line은 0의 foldlevel을 가진다!

그 이유는 10 times(1): 줄에 대해서 명시적으로 1 foldlevel을 주지 않았기 때문이다. 우리는 >1을 사용하여 vim에게 해당 줄이 1 foldlevel일 때 열리게 하였다. vim은 이것이 인접한 undefined 줄들에 대해서 foldlevel을 1 대신 0을 사용하게 할 만큼 똑똑하다.

정확한 로직은 vim의 소스코드 깊숙히 묻혀있다. 보통 vim은 꽤 똑똑해서 "specia" foldlevel에 인접한 undefined 라인을 당신이 원하는대로 처리해준다.

vim이 undefined 줄들의 처리까지 마치면 각각의 줄의 foldlevel은 다음과 같아진다.

factorial = (n):                         1
    total = 1                            1
    n to 1 (i):                          2
        # Multiply the running total.    2
        total *= i.                      2
    total.                               1
                                         0
10 times (i):                            1
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.                          1

드디어 끝났다! folding code를 리로드 (:set ft=potion)한 후에 factorial.pn을 가지고 fold를 다뤄보자.

Exercises

  • Read :help foldexpr.

  • Read :help fold-expr. Pay particular attention to all the "special" strings your expression can return.

  • Read :help getline.

  • Read :help indent().

  • Read :help line().

  • Figure out why it's important that we use . to combine the > character with the number in our folding function. What would happen if you used + instead? Why?

+를 사용하면 >1이 아니라 1이 연산된다. 다음 코드로 테스트 가능

:echom ">" . 1
>1
:echom ">" + 1
1
  • We defined our helper functions as global functions, but that's not a good idea. Change them to be script-local functions.

s:, <SID>를 사용하여 쉽게 변경가능.

...

function! s:NextNonBlankLine(lnum)
    ...

function! s:IndentLevel(lnum)
    ...


    ...
    let this_indent = <SID>IndentLevel(a:lnum)
    let next_indent = <SID>IndentLevel(<SID>NextNonBlankLine(a:lnum))
    ...
  • Put this book down and go outside for a while to let your brain recover from this chapter.