tools/vim

Learn Vimscript The Hard Way - Advanced Folding : part 2

seul chan 2020. 5. 2. 19:15

Advanced Folding - 2/3

해당 장은 매우 길기 때문에 파트를 나눠서 작성할 예정이다. 이전 장을 읽고 이어 읽기를 추천한다.

Blank Lines

우선 특수한 케이스인 빈 줄을 먼저 살펴보자. GetPotionFold을 다음과 같이 수정하자.

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

    return '0'
endfunction

우리는 if절을 추가하여 빈 줄(blank line)에 대해 처리하였다. 어떻게 동작하는 것일까?

먼저 우리는 getline(a:lnum)을 사용하여 현재 줄의 내용을 string으로 받았다.

이를 '\v^\s*$' 정규표현식과 비교하였다. \v는 "very magic" 정규표현식 모드인 것을 기억하라. 이 정규표현식은 "줄의 처음, 어떤 숫자의 whitespace, 줄의 마지막"을 매칭시킨다. (띄어쓰기만 0개 이상인 줄)

비교는 case-insensitive 매치 오퍼레이터인 =~?을 사용하였다. 이는 whitespace와만 매칭되기 때문에 엄밀이 말하면 대소문자는 고려할 필요가 없지만, 나(저자)는 모든 string 비교 연산자에 명시적인 표현식을 사용하는 것을 선호하기 때문에 이를 사용하였다. 취향에 따라 =~를 사용해도 무방하다.

vim의 정규표현식에 대해서 refresh가 필요하면 이전 장인 "Basic Regular Expressions"(블로그 포스트)를 참고하라.

현재 라인에 whitespace가 아닌 캐릭터가 있다면 이는 매치되지 않을 것이고 이전과 같이 '0'을 반환한다.

만약 현재 줄이 정규표현식과 매칭된다면 '-1' 스트링을 반환할 것이다.

이전 설명에서는 각 줄의 foldlevel은 0이나 양의 정수밖에 안 된다고 하였는데, 무슨일이 일어난 것일까?

Special Foldlevels

custom folding 표현식은 foldlevel을 직접 설정할 수도 있지만, vim에게 특정 레벨을 지정하지 않는다고 얘기해 줄 수 있는 "special" string을 지정할 수도 있다.

'-1'은 special string중 하나이다. 이는 vim에게 해당 줄의 level은 "undefined"라고 말해준다. vim은 이것을 이렇게 해석한다. "해당 줄의 foldlevel은 이전이나 이후 줄의 foldlevel 중 더 작은 것과 같다"

이는 우리의 계획과 완전히 같지는 않지만 빗스하고, 이후에 우리가 원하는 것을 할 것이다.

vim은 이 undefined lines들을 함께 "chain" 할 수 있기 때문에, 만약 level 1로 이어진 두 줄이 있다면 마지막 undefined line을 1로 세팅할 것이다.

custom folding 코드를 작성할 때 종종 정확히 레벨을 특정지을 수 없는 라인들을 발견할 것이다. 그 때 '-1'(과 이후에 볼 다른 special foldlevel)을 사용하여 다른 파일을 참고하여 맞는 레벨을 "cascade" 할 수 있다.

factorial.pn을 reload(:set ft=potion)하면 vim은 여전히 아무 줄도 fold하지 않을 것이다. 이는 foldlevel이 0이거나 "undefind"이기 때문이다. 0레벨은 다른 undefined 줄을 cascade하여 모든 foldlevel을 0으로 만들어준다.

An Identation Level Helper

non-blank line을 해결하기 위해서 우리는 그들의 indent level을 알아야 하기 때문에 이를 계산하는 작은 헬퍼 함수를 만들것이다. 다음 함수를 GetPotionFold 위에 추가해보자.

function! IndentLevel(lnum)
    return indent(a:lnum) / &shiftwidth
endfunction

folding code를 리로드 하고, factiorial.pn 버퍼에서 테스트해보자.

:echom IndentLevel(1)
0
:echom IndentLevel(2)
1
:echom IndentLevel(3)
2

IndentLevel 함수는 꽤 단순하다. indent(a:lnum)은 해당 번호의 줄의 시작에 나타나는 space의 개수를 반환한다. 우리는 이를 버퍼의 shiftwidth로 나눠서 indentation의 레벨을 구하였다.

왜 4로 나누는 대신 &shiftwidth를 사용하였을까? 만약 어떤 사용자가 Potion 파일에 대해 2칸 들여쓰기를 사용하면, 4로 나누는 것은 부정확한 결과를 불러일으킬 수 있다. 우리는 모든 space level에서도 사용할 수 있게 하기 위해 shiftwidth를 사용하였다.

One More Helper

이제부터 무엇을 해야할지는 명확하지 않다. 잠시 멈추고 우리가 non-blank 줄들을 fold하기 위해서 어떤 정보가 필요한지 생각해보자.

우리는 우선 해당 줄의 indentation level을 알아야 한다. 이는 IndentLevel 함수로 해결되었다.

우리는 indented 된 본문과 "header"도 같이 fold해야 하기 때문에 다음 non-blank 줄의 indentation level도 필요하다.

IndentLevel 함수 위에 다음 non-blank 줄 (next non-blank line)의 줄 번호를 알려주는 헬퍼 함수를 만들어보자.

function! NextNonBlankLine(lnum)
    let numlines = line('$')
    let current = a:lnum + 1

    while current <= numlines
        if getline(current) =~? '\v\S'
            return current
        endif

        let current += 1
    endwhile

    return -2
endfunction

이 함수는 조금 길지만, 간단하다. 이를 나눠서 살펴보자.

우선 우리는 파일의 전체 줄 길이를 line('$')를 사용하여 저장하였다. line()의 documentation을 확인하여 어떻게 동작하는지 살펴보자. (:help line())

그 다음에 우리는 다음 줄의 번호를 current 변수에 세팅하였다.

이후 loop를 시작하여 각 줄을 돌린다.

만약 해당 줄이 \v\S (whitespace가 아닌 모든 문자)와 매칭되면 non-blank 라인일 것이고, 해당 줄의 번호를 반환한다.

만약 매칭되지 않으면, 다음 줄로 loop문을 넘긴다.

만약 loop문이 아무런 반환 없이 파일의 끝까지 진행되었다면 더 이상 현재 줄 이후에 non-blank line이 없다는 말이다. 그러면 -2를 반환한다. 이는 유효한 줄 번호가 아니기 때문에 "유효한 결과가 없다"는 것을 말해주는 쉬운 방법이다.

-1 또한 유효하지 않기 때문에 이를 반환할 수도 있다. 심지어 vim의 줄은 1부터 시작하기 때문에 0을 고를 수도 있었을 텐데 왜 -2를 골랐을까?

-2를 고른 이유는 우리가 folding code를 작업하고 있기 때문이고, '-1''0'은 special foldlevel string이기 때문이다.

만약 파일을 읽다가 -1을 발견하면 나의 뇌는 즉시 이를 "undefined foldlevel"이라고 생각하게 될 것이다. 이는 0에도 마찬가지다. 나(저자)는 -2를 골랐는데, 이는 foldlevel이 아니라 "error"라는 것을 아주 명확하게 보여준다.

이것이 이상하다면, -20이나 -1로 변경해도 상관 없다. 이는 그냥 코드 작성 선호도일 뿐이다.

이후에 fold 함수 완성은 다음 포스트에 작성할 예정이다.