콘텐츠로 건너뛰기
Home » vim에서 터미널로 코드를 실행하기

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

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

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

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

function! s:run_script() abort
        " 현재 버퍼가 파이썬 파일이 아니면 실행하지 않는다.
	if &l:filetype !~# 'python'
		return
	endif
	" 이전에 실행해서 열어둔 결과창이 있으면 닫는다.
	let commands = ['python', expand('%:p')->fnameescape()]
	if exists('b:outbufnr') && bufwinnr(b:outbufnr) > -1
		exec bufwinnr(b:outbufnr) . 'hide'
	endif

        " 빈 새 버퍼를 만들고, 그 번호를 outbufnr 에 기억해둔다.
	let b:outbufnr = bufadd('')
	let temp = bufnr()
        " 새 버퍼를 분할창에 표시한다. 이제 새 버퍼로 옮겨온 상태이고, 버퍼를 설정한다.
	exec 'belowright sbuffer ' .. b:outbufnr
	let b:srcbufnr = temp
	setlocal buftype=nofile nobuflisted bufhidden=wipe noswapfile nowritebackup
	nnoremap <buffer><silent> <Esc> <Cmd>hide<CR>
        " job을 실행하고 결과를 버퍼에 표시하도록 한다.
	let jobid = job_start(commands, #{
				\out_io: 'buffer',
				\out_buf: bufnr(),
				\})
endfunction

" 키 맵을 설정해놓는다.
nnoremap <Plug>(run_python) <Cmd> call <SID>run_script()<CR>
nmap <F7> <Plug>(run_python)

위 vim 스크립트를 파일로 저장한 후, so % 명령으로 로딩한다. 이후 파이썬 파일을 열고 <F7> 키를 누르면 아래쪽으로 창이 열리고 출력되는 값들이 여기에 표시된다.

해설

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

  1. 현재 버퍼의 정보를 사용해서 실행할 명령을 미리 생성해 놓고 이를 commands 변수에 저장한다.
  2. 만약 이전에도 이 파일에 대해서 실행을 한 적이 있다면 b:outbufnr 이라는 변수가 있을 것이다. 이 변수에 대한 창이 보이고 있는 상태라면 닫아야 한다. bufwinnr() 함수는 특정 버퍼를 출력하고 있는 창 번호를 리턴하는데, 이 값이 0보다 작으면 버퍼를 표시하는 창이 없다는 의미이다. 그렇지 않은 경우에는 {창번호}hide 를 실행해서 창을 가린다. 뒤에서 설정하는 내용이 있지만, 결과 창은 숨겨질 때 그 버퍼의 내용이 제거된다.
  3. addbuf('') 함수를 통해서 새 버퍼를 백그라운드에서 만들고 해당 버퍼 번호를 얻을 수 있다. 이를 b:outbufnr 에 저장해둔다. b: 로 시작하는 변수들은 모두 특정 버퍼에서만 통용된다.
  4. 새로 만든 버퍼를 화면에 보이게하려면 sbuffer {bufnr} 명령을 사용하면 된다. 아래쪽 기준으로 분할창을 만들기 위해서 belowright 와 함께 사용한다.
  5. 버퍼를 분할하고나면 새 버퍼창으로 진입한 상태가 된다. 여기서 setlocal 을 사용해서 버퍼에 대한 몇 가지 설정을 한다. 이 버퍼는 저장할 용도가 아닌 단순한 출력용의 창이다. bufhidden=wipe 를 설정해두면 버퍼가 보이지 않게 되었을 때 내용을 제거한다는 의미이다.
  6. 새 버퍼에서 Esc 키를 누르면 닫히도록 (:hide) 키맵을 정의한다. 이 때 <buffer> 옵션을 사용해서 해당 버퍼에서만 사용되도록 한다.
  7. 다시 소스 버퍼로 넘어왔으면, 여기서 현재 파일 경로를 파이썬에게 넘겨서 백그라운드 작업을 시작한다. 이 때 job의 옵션에 out_io 키를 'buffer'로 주면 버퍼의 입력을 job의 출력과 연결할 수 있다.
  8. 작업 시작한 후 다시 출력 버퍼로 이동한다.

입력이 가능한 버전

이상의 코드는 출력 결과만을 버퍼로 보여준다. 따라서 적절한 파일 타입에 대해서, commands 값만 잘 세팅한다면 다른 종류의 스크립트의 실행이나, 컴파일 결과 등을 결과창에서 볼 수 있게 만들 수 있다.

하지만 이 방법은 ‘출력’만을 담고 있는 창을 만들게 된다. 만약 파이썬 스크립트가 input() 함수를 써서 키보드 입력을 받는다면, 이 방법은 사용할 수 없다. 실제 IDE가 하는 것처럼 실행창에서 값을 직접 입력할 수 있는 형태로 구현할 수는 없을까? vim 8부터는 아예 터미널 창을 만들 수 있게도 되었으니, 이를 사용하겠다. 터미널 창은 백그라운드에서 job을 실행하면서 이 프로세스의 표준입출력을 버퍼와 연결하는 것이다.

  1. 터미널 창을 열 때, 인자로 실행될 명령을 주고, 대신 job_start() 함수를 사용하지 않는다.
  2. 터미널 버퍼는 숨겨질 때 제거되어야 하므로 setl bufhidden=delete로 옵션을 준다.
  3. 터미널에서의 입력모드 (터미널모드)에서 빠져나올 필요가 있을 때 <Esc> 키를 누를 수 있도록 맵핑을 추가해준다.
  4. 터미널이 종료될 때 콜백을 실행하고 싶다면, 터미널 버퍼에서 term_getjob('%')을 사용해서 해당 터미널의 job id를 얻어서, job_setoptions() 함수에서 exit_cb 옵션을 지정해주면 된다.
function! s:run_in_terminal() abort
	if &l:filetype !~# 'python' || exists('b:srcbufnr')
		return
	endif
	" close output window that is preivously opened
	let commands = ['python', expand('%:p')->fnameescape()]
	if exists('b:outbufnr')
		exec bufwinnr(b:outbufnr) . 'hide'
	endif

	let temp = bufnr()
	let b:outbufnr = term_start(commands)
	
	" now you are in new buffer
	let b:srcbufnr = temp
	setlocal nobuflisted bufhidden=delete noswapfile nowritebackup
	nnoremap <buffer><silent> <Esc> <C-\><C-n>:<C-u>hide<CR>
endfunction

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

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