Learn Vimscript The Hard Way - Advanced Folding : part 2
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"라는 것을 아주 명확하게 보여준다.
이것이 이상하다면, -2
를 0
이나 -1
로 변경해도 상관 없다. 이는 그냥 코드 작성 선호도일 뿐이다.
이후에 fold 함수 완성은 다음 포스트에 작성할 예정이다.