vim에서 터미널로 코드를 실행하기

vim에서 파이썬이나 자바스크립트 코드를 작성하고 바로 실행하는 가장 쉬운 방법은 :!python % 과 같이 외부 명령을 바로 호출하는 것이다. 이 경우 vim은 잠시 숨겨지고 해당 명령을 실행하는 상태로 화면이 전환된다.

보다 IDE와 비슷한 느낌(느낌 알잖아요…)이 중요하다면 vim내에서 창을 나누고 그 창에서 실행 결과를 보는 방법이 있다. vim8에서 도입된 job 기능을 사용하여 백그라운드에서 해당 프로그램이 실행되면 그 결과를 새 버퍼로 받아서 보여주는 방법도 있고, 또 아예 :term 명령으로 해당 파일을 여는 방법도 있다.

새로운 vim 스크립트 파일을 하나 생성하고 아래와 같이 작성해보자.

" job이 끝났을 때 알려주는 콜백
function! s:callback_exit(jobid, status) abort
  call popup_notification((a:status == 0 ? 'Done' : 'Error'), {})
endfunction

function! s:run_python_file() abort
  " 파이썬이 아니면 실행하지 않는다.
  if &l:filetype !~# 'python' | return | endif

  " 새 버퍼를 만들기
  let b:outname = 'out_' . expand('%:t:r')
  exec 'belowright new ' . b:outname
  
  " new 명령으로 새 창을 만들었으며, 현재는 새창에 있는 상태
  " 새 버퍼 설정
  let l:temp = bufnr()
  setl buftype=nofile nobuflisted nobackup bufhidden=wipe
  nmap <buffer><silent> <Esc> :<c-u>hide<CR>
  
  " 이전 창으로 돌아가기
  exec bufwinnr(bufnr('#')) . 'wincmd w'
  
  " job 실행
  let b:outbf = l:temp
  let b:job = job_start(['python', expand('%:p')]->join(' ' ), #{
                \out_io: 'buffer',
                \out_buf: b:outbf,
                \exit_cb: function('s:callback_exit')})

  " 결과창으로 이동하기
  exec b:outbf . 'wincmd w'
endfunction

" 실행명령 정의
command! -nargs=0 Test call <SID>run_python()

위 vim 스크립트를 파일로 저장한 후, so % 명령으로 로딩한다. 이후 파이썬 파일을 열고 :Test를 실행하면 창이 하나 분할되고, 해당 파이썬 프로그램에서 출력한 내용이 버퍼에 담기게 된다.

해설

코드가 동작하는 순서는 다음과 같다.

  1. 현재 파일 이름에서 ‘out_’을 앞에붙이고 확장자를 뗀 이름만 사용해서 버퍼의 이름을 정한다. 이 이름은 해당 버퍼 범위의 변수로 만드는데, 반복 실행될 때 유지하기 위해서이다.
  2. belowright new 버퍼이름 명령을 실행해서 새 버퍼를 아래나 오른쪽에 만든다.
  3. :new 명령으로 새 버퍼를 만들면, 그 시점부터 이후 스크립트가 그 새로운 버퍼에서 실행되는 것처럼 간주된다. 따라서 bufnr() 함수는 새 버퍼의 번호를 리턴하며, b: 스코프를 가진 변수들은 모두 새 버퍼의 것을 따른다.
  4. 새 버퍼를 설정한다. 파일로 기록되지 않는 nofile 타입의 버퍼 타입을 지정하고, nobuflisted 를 설정하여 버퍼 목록에 보이지 않도록 한다. 또 bufhidden=wipe 옵션을 주어 버퍼가 화면에서 사라지면 제거되도록 한다.
  5. 새 버퍼에서 Esc 키를 누르면 닫히도록 (:hide) 키맵을 정의한다. 이 때 <buffer> 옵션을 사용해서 해당 버퍼에서만 사용되도록 한다.
  6. 현재 버퍼의 번호를 함수 로컬 스코프 범위에 저장한다. 이렇게하면 원래 버퍼로 돌아갔을 때, 현재 버퍼의 번호를 사용할 수 있다.
  7. 파이썬 파일인 원래 버퍼는 현 시점에서 '#'로 참조된다. bufwinnr() 함수를 사용하여 이전 버퍼의 창 번호를 알 수 있다. 참고로 winbufnr(), bufwinnr() 함수가 엄청 헷갈리는데 buf->winnr()로 이해하면 된다. 버퍼를 창번호로 변환한다고 이해할 수 있다.
  8. {winnr}wincmd w 명령을 사용하면 해당 창으로 이동할 수 있다. wincmd 명령은 노멀모드에서 <c-w> 명령 과 동일하다.
  9. 다시 소스 버퍼로 넘어왔으면, 여기서 현재 파일 경로를 파이썬에게 넘겨서 백그라운드 작업을 시작한다. 이 때 job의 옵션에 out_io 키를 'buffer'로 주면 버퍼의 입력을 job의 출력과 연결할 수 있다.
  10. 작업 시작한 후 다시 출력 버퍼로 이동한다.

재사용이 용이한 버전으로 개선

이렇게 작성한 함수를 사용자 정의 명령으로 호출하면 파이썬 파일을 실행하고 그 출력값을 새 버퍼에 표시할 수 있다. 문제는 이 명령이 :new 를 사용하기 때문에 호출할 때마다 새 버퍼를 계속해서 만든다는 점이다. (쉽게 닫기 위해서 Esc키를 맵핑하긴 했지만…)

따라서 재호출시에 이전 실행 결과창이 열려있으면 닫도록 하는 명령을 추가해보도록 하자. 이전에 작성한 코드중에서는 소스 버퍼로 돌아왔을 때, let b:out_bf = l:temp 를 통해서 출력창의 버퍼 번호를 소스 버퍼 범위 변수에 저장했다. 이 값을 검사해서 창을 닫는다.

function! s:run_python_file() abort
  " 파이썬이 아니면 실행하지 않는다.
  if &l:filetype !~# 'python' | return | endif

  " 이전에 만든 결과 버퍼가 있으면 닫기
  if exists('b:bf_out') && winbufnr(b:bf_out) != -1
    exec b:bf_out . 'wincmd q'

  " 새 버퍼를 만들기
  ....

입력이 가능한 버전

이상의 코드는 출력 결과만을 버퍼로 보여준다. 만약 파이썬 스크립트가 input() 함수를 써서 키보드 입력을 받는다면, 이 방법은 사용할 수 없다. 이 때는 :new 명령대신 :term 명령을 써서 새 터미널 창을 열면 된다. 단 몇 가지 설정에 주의할 것이 있다.

  1. 터미널 창을 열 때, 인자로 실행될 명령을 주고, 대신 job_start() 함수를 사용하지 않는다.
  2. 터미널 버퍼는 숨겨질 때 제거되어야 하므로 setl bufhidden=delete로 옵션을 준다.
  3. 터미널에서의 입력모드 (터미널모드)에서 빠져나올 필요가 있을 때 <Esc> 키를 누를 수 있도록 맵핑을 추가해준다.
  4. 터미널이 종료될 때 콜백을 실행하고 싶다면, 터미널 버퍼에서 term_getjob('%')을 사용해서 해당 터미널의 job id를 얻어서, job_setoptions() 함수에서 exit_cb 옵션을 지정해주면 된다.
function! s:run_python_term() abort
        " 파이썬 파일일 때만 실행
	if &l:filetype !~# 'python' | return | endif
        " 이전 실행창이 열려있으면 닫기
	if exists('b:buf_output') && bufwinnr(b:buf_output) != -1
		exec bufwinnr(b:buf_output) . 'wincmd q'
	endif
	let b:cmd_args = ['python', expand('%:p')]
	" 터미널 실행 및 설정
	exec 'belowright term ' . b:cmd_args->join(' ')
	" now on out_buffer
	let l:temp = bufnr() " 터미널 버퍼 번호
	setl nobackup nobuflisted bufhidden=delete
        " 터미널 입력 중 Esc를 눌러서 노멀모드로 빠져나오기
        tnoremap <buffer><silent> <Esc> <c-\><c-n>
	nnoremap <buffer><silent> <Esc> :<c-u>hide<CR>
	nnoremap <buffer><silent> q :<c-u>hide<CR>
	let l:termjob = term_getjob('%')
      	" 원본버퍼로 돌아가서 b:buf_output 을 설정해준다.
	exec bufwinnr(bufnr('#')) . 'wincmd w'
	let b:buf_output = l:temp
	call job_setoptions(l:termjob, #{
			\exit_cb: function('s:callback_exit')})
	" 터미널로 복귀
	exec bufwinnr(b:buf_output) . 'wincmd w'
endfunction

명령 및 맵핑을 버퍼 범위로 지정하고, 위 코드를 ftplugin/python.vim 등에 기록해두면 파이썬 파일을 열 때마다 맵핑이 지정되어 호출할 수 있다. 이전에 소개했던 간단한 맵핑과는 달리, 이를 통해서 작성된 코드를 vim을 벗어나지 않고 분할창에서 실행할 수 있는 기능을 사용할 수 있다.

실제로 이 방식은 작성할 코드를 터미널에서 실행할 명령만 구성하면 자바스크립트나 그외 어떤 다른 언어에 대해서도 동일한 기능을 구성할 수 있기에 얼마든지 확장하여 사용할 수도 있을 것이다. 개인적으로는 autoload 폴더 속에 실제 분할 및 터미널 실행 코드를 함수로 작성해두고, 실행할 명령과 콜백들을 ftplugin 폴더에 파일 타입별로 정의하고, 버퍼별 맵핑을 지정하여 사용하고 있다.