tools/vim

Learn Vimscript The Hard Way - 52. External Commands - part two

seul chan 2020. 5. 8. 22:33

External Commands - part two

해당 장도 두 포스트로 나누어서 작성하였다. 1부를 읽고 읽기를 권장한다.

Displaying Bytecode

Potion 컴파일러는 컴파일 되었을 때 생성되는 bytecode를 볼 수 있게 해주는 옵션을 제공한다. 이는 아주 low-level에서 디버깅을 하기 쉬워진다. 쉘에서 다음 명령어를 실행해보자.

$ potion -c -V factorial.pn

-- parsed --
code ...
-- compiled --
; function definition: 0x109d6e9c8 ; 108 bytes
; () 3 registers
.local factorial ; 0
.local print_line ; 1
.local print_factorial ; 2
...
[ 2] move     1 0
[ 3] loadk    0 0   ; string
[ 4] bind     0 1
[ 5] loadpn   2 0   ; nil
[ 6] call     0 2
...

쉽게 볼 수 있게 현재 vim에 스플릿된 창에 현재 potion 파일이 생성하는 bytecode를 띄워주는 매핑을 추가해보자.

먼저, ftplugin/potion/running.vim의 가장 아래에 다음을 추가하자.

nnoremap <buffer> <localleader>b :call PotionShowBytecode()<cr>

특별할 것 없는 그냥 매핑이다. 이제 함수를 스케치해보자.

function! PotionShowBytecode()
    " Get the bytecode.

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

이제 기본적인 구조는 갖춰졌다. 어떻게 실행할지에 대해 이야기해보자.

system()

아주 많은 방법들이 있지만, 가장 간단한 방법을 사용할 것이다.

다음 명령어를 실행시켜보자.

:echom system("ls")

스크린의 아래에 ls 명령어의 결과물이 나올 것이다. :messages 명령어를 실행시켜도 결과물을 볼 수 있다. system() 함수는 명령어 스트링을 인자로 받아서 해당 결과물을 string으로 반환해주는 함수이다.

system() 함수의 두 번째 인자를 넘길 수 있다. 다음을 실행시켜보자.

:echom system("wc -c", "abcdefg")

vim은 (패딩이 추가된) 7을 반환할 것이다. 만약 두 번째 인자가 추가되면 vim은 temporary 파일을 만들어서 이를 standard input으로 넘겨준다. 우리의 목적에는 별로 맞지 않지만, 알아두면 좋을 것이다.

다시 우리 함수로 돌아와 PotionShowBytecode()를 수정해보자.

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))
    echom bytecode

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

파일을 저장하고 factorial.pn에서 :set ft=potion을 실행시킨 후 <localleader>b 매핑을 실행시켜보자. 스크린 아래에 bytecode를 출력하는 것을 볼 수 있을 것이다.

Scratch Splits

이제 새 창을 열어서 해당 결과를 유저에게 보여줄 것이다. 이는 사용자가 bytecode를 그냥 스크린으로 읽는 것이 아니라 강력한 vim으로 navigate 할 수 있게 해 줄 것이다.

이를 위해 우리는 "scratch" split을 만들 것이다: 이는 절대 저장되지 않고 매번 매핑이 실행될 때마다 덮어써지는 버퍼를 가진 split을 말한다. PotionShowBytecode()를 다음과 같이 수정해보자.

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.

endfunction

vsplit__Potion_Bytecode__라는 이름을 가진 버퍼를 vertical split에 띄워준다. 우리는 이 이름에 던더(__)를 넣어서 보통 파일이 아니라는 것을 알기 쉽게 하였다. 이는 그냥 컨벤션이다.

다음으로, normal! ggdG를 사용해서 버퍼에 있는 모든 내용을 지웠다. 처음 매핑이 실행되면 아무것도 없을테지만, __Potion_Bytecode__를 다시 사용하면 이를 모두 지워야 한다.

그리고 두 로컬 세팅을 추가하였다. 첫 번째는 filetype을 potionbytecode로 하는 것이다. 또한 buftypenofile로 하여 vim이 해당 버퍼가 disk의 파일과는 전혀 상관이 없고, 저장하지 않게 해 주었다.

이제 bytecode 변수로 저장한 bytecode를 버퍼에 쓰는 것만 남았다. 함수를 다음과 같이 완성시켜보자.

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%") . " 2>&1")

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.
    call append(0, split(bytecode, '\v\n'))
endfunction

append() 함수는 두가지 인자를 받는다: 추가할 줄 넘버와, 추가할 String의 리스트이다. 이해를 위해서 다음 명령어를 실행시켜보자.

:call append(3, ["foo", "bar"])

3번째줄 이후에 foo, bar가 각각 4번째, 5번째 줄로 추가된 것을 볼 수 있다. 0을 인자로 사용하면 "파일의 가장 맨 위"에 추가된다.

우리는 추가할 스트링의 리스트가 필요하기 때문에 vim의 split() 함수를 사용하여 텍스트를 리스트로 쪼갰다. split()은 텍스트와 정규표현식을 인자로 받는다.

함수가 완성되었으니 이를 저장하고 매핑을 사용해보자. <localleader>bfactorial.pn에서 사용하면 새 버퍼를 열어서 Potion bytecode를 반환할 것이다.

Exercises

  • Read :help bufname.

  • Read :help buftype.

  • Read :help append().

  • Read :help split().

  • Read :help :!.

  • Read :help :read and :help :read! (we didn't cover these commands, but they're extremely useful).

  • Read :help system().

  • Read :help design-not.

  • Currently our mappings require that the user save the file themselves before running the mapping in order for their changes to take effect. Undo is cheap these days, so edit the functions we wrote to save the current file for them.

  • What happens when you run the bytecode mapping on a Potion file with a syntax error? Why does that happen?

  • Change the PotionShowBytecode() function to detect when the Potion compiler returns an error, and show an error message to the user.

Extra Credit

이전 창이 열려 있음에도 불구하고 매번 bytecode mapping을 실행할 때마다 새로운 vertical split이 생성된다. 창을 닫지 않는다면 아주 많은 윈도우가 쌓일 것이다.

PotionShowBytecode()를 변경하여 이미 __Potion_Bytecode__ 버퍼가 윈도우에 열려있 새로운 split을 생성하는 대신 기존 윈도우를 사용하게 해보자.

:help bufwinnr()를 읽을 필요가 있을것이다.

More Extra Credit

우리의 임시 버퍼의 filtypepotionbytecode로 설정한 것을 기억할 것이다. 이를 더 읽기 쉽게 하기 위해서 syntax/potionbytecode.vim 파일을 만들고 몇몇 syntax highlighting을 추가해보자.

External Commands - part two

해당 장도 두 포스트로 나누어서 작성하였다. 1부를 읽고 읽기를 권장한다.

Displaying Bytecode

Potion 컴파일러는 컴파일 되었을 때 생성되는 bytecode를 볼 수 있게 해주는 옵션을 제공한다. 이는 아주 low-level에서 디버깅을 하기 쉬워진다. 쉘에서 다음 명령어를 실행해보자.

$ potion -c -V factorial.pn

-- parsed --
code ...
-- compiled --
; function definition: 0x109d6e9c8 ; 108 bytes
; () 3 registers
.local factorial ; 0
.local print_line ; 1
.local print_factorial ; 2
...
[ 2] move     1 0
[ 3] loadk    0 0   ; string
[ 4] bind     0 1
[ 5] loadpn   2 0   ; nil
[ 6] call     0 2
...

쉽게 볼 수 있게 현재 vim에 스플릿된 창에 현재 potion 파일이 생성하는 bytecode를 띄워주는 매핑을 추가해보자.

먼저, ftplugin/potion/running.vim의 가장 아래에 다음을 추가하자.

nnoremap <buffer> <localleader>b :call PotionShowBytecode()<cr>

특별할 것 없는 그냥 매핑이다. 이제 함수를 스케치해보자.

function! PotionShowBytecode()
    " Get the bytecode.

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

이제 기본적인 구조는 갖춰졌다. 어떻게 실행할지에 대해 이야기해보자.

system()

아주 많은 방법들이 있지만, 가장 간단한 방법을 사용할 것이다.

다음 명령어를 실행시켜보자.

:echom system("ls")

스크린의 아래에 ls 명령어의 결과물이 나올 것이다. :messages 명령어를 실행시켜도 결과물을 볼 수 있다. system() 함수는 명령어 스트링을 인자로 받아서 해당 결과물을 string으로 반환해주는 함수이다.

system() 함수의 두 번째 인자를 넘길 수 있다. 다음을 실행시켜보자.

:echom system("wc -c", "abcdefg")

vim은 (패딩이 추가된) 7을 반환할 것이다. 만약 두 번째 인자가 추가되면 vim은 temporary 파일을 만들어서 이를 standard input으로 넘겨준다. 우리의 목적에는 별로 맞지 않지만, 알아두면 좋을 것이다.

다시 우리 함수로 돌아와 PotionShowBytecode()를 수정해보자.

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))
    echom bytecode

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

파일을 저장하고 factorial.pn에서 :set ft=potion을 실행시킨 후 <localleader>b 매핑을 실행시켜보자. 스크린 아래에 bytecode를 출력하는 것을 볼 수 있을 것이다.

Scratch Splits

이제 새 창을 열어서 해당 결과를 유저에게 보여줄 것이다. 이는 사용자가 bytecode를 그냥 스크린으로 읽는 것이 아니라 강력한 vim으로 navigate 할 수 있게 해 줄 것이다.

이를 위해 우리는 "scratch" split을 만들 것이다: 이는 절대 저장되지 않고 매번 매핑이 실행될 때마다 덮어써지는 버퍼를 가진 split을 말한다. PotionShowBytecode()를 다음과 같이 수정해보자.

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.

endfunction

vsplit__Potion_Bytecode__라는 이름을 가진 버퍼를 vertical split에 띄워준다. 우리는 이 이름에 던더(__)를 넣어서 보통 파일이 아니라는 것을 알기 쉽게 하였다. 이는 그냥 컨벤션이다.

다음으로, normal! ggdG를 사용해서 버퍼에 있는 모든 내용을 지웠다. 처음 매핑이 실행되면 아무것도 없을테지만, __Potion_Bytecode__를 다시 사용하면 이를 모두 지워야 한다.

그리고 두 로컬 세팅을 추가하였다. 첫 번째는 filetype을 potionbytecode로 하는 것이다. 또한 buftypenofile로 하여 vim이 해당 버퍼가 disk의 파일과는 전혀 상관이 없고, 저장하지 않게 해 주었다.

이제 bytecode 변수로 저장한 bytecode를 버퍼에 쓰는 것만 남았다. 함수를 다음과 같이 완성시켜보자.

function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%") . " 2>&1")

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.
    call append(0, split(bytecode, '\v\n'))
endfunction

append() 함수는 두가지 인자를 받는다: 추가할 줄 넘버와, 추가할 String의 리스트이다. 이해를 위해서 다음 명령어를 실행시켜보자.

:call append(3, ["foo", "bar"])

3번째줄 이후에 foo, bar가 각각 4번째, 5번째 줄로 추가된 것을 볼 수 있다. 0을 인자로 사용하면 "파일의 가장 맨 위"에 추가된다.

우리는 추가할 스트링의 리스트가 필요하기 때문에 vim의 split() 함수를 사용하여 텍스트를 리스트로 쪼갰다. split()은 텍스트와 정규표현식을 인자로 받는다.

함수가 완성되었으니 이를 저장하고 매핑을 사용해보자. <localleader>bfactorial.pn에서 사용하면 새 버퍼를 열어서 Potion bytecode를 반환할 것이다.

Exercises

  • Read :help bufname.

  • Read :help buftype.

  • Read :help append().

  • Read :help split().

  • Read :help :!.

  • Read :help :read and :help :read! (we didn't cover these commands, but they're extremely useful).

  • Read :help system().

  • Read :help design-not.

  • Currently our mappings require that the user save the file themselves before running the mapping in order for their changes to take effect. Undo is cheap these days, so edit the functions we wrote to save the current file for them.
    normal! :w를 함수에 추가하였다.

  • What happens when you run the bytecode mapping on a Potion file with a syntax error? Why does that happen?
    ** Syntax error before text "factorial = (n):" 에러 메세지가 반환된다.

  • Change the PotionShowBytecode() function to detect when the Potion compiler returns an error, and show an error message to the user.
    이미 에러 메세지가 반환되는데 왜 수정하라고 하는건지 잘 모르겠지만..

Extra Credit

이전 창이 열려 있음에도 불구하고 매번 bytecode mapping을 실행할 때마다 새로운 vertical split이 생성된다. 창을 닫지 않는다면 아주 많은 윈도우가 쌓일 것이다.

PotionShowBytecode()를 변경하여 이미 __Potion_Bytecode__ 버퍼가 윈도우에 열려있 새로운 split을 생성하는 대신 기존 윈도우를 사용하게 해보자.

:help bufwinnr()를 읽을 필요가 있을것이다.

다음 줄을 추가해주었다.

" Open a new split and set it up
let bytecode_window_number = bufwinnr("__Potion_Bytecode__")
if bytecode_window_number != -1
    execute bytecode_window_number . "wincmd w"
else
    vsplit __Potion_Bytecode__
endif

More Extra Credit

우리의 임시 버퍼의 filtypepotionbytecode로 설정한 것을 기억할 것이다. 이를 더 읽기 쉽게 하기 위해서 syntax/potionbytecode.vim 파일을 만들고 몇몇 syntax highlighting을 추가해보자.