I'm configuring a terminal buffer and now I have the following problem. I have only one terminal buffer and I need to make it active by its number to avoid lots of CTRL-w j/k/l clicks.

Here is what I tried:

  1. Given vim opened I typed :bot term to create a new terminal buffer:

enter image description here

Here we have a terminal buffer opened on the bottom.

  1. I go to the top buffer and try to make terminal focused by its buffer number to type some commands (this particular case is simple, but in general navigation might be complicated).

I type the following commands right in vim one by one

:let g:tn = bufnr("$")
:echo g:tn "prints 2
:b 2

And here is where the problem comes. Instead of just activating the terminal I got 2 synchronized terminal buffers:

enter image description here

So the top buffer became "a mirror" of the terminal buffer with the number 2.

Is there a way to fix it? So just activate a terminal to type some more commands?


Make a buffer active by its number

You can combine win_gotoid() & win_findbuf() to accomplish this:

:call win_gotoid(get(win_findbuf(g:tn), 0))

But we can do better for terminal like things by improving a few things:

  • Automatically set our variable on TerminalOpen autocmd
  • Provide methods to send text to the terminal
  • Provide a command to jump to the term or open it

Add the following to your vimrc or better yet ~/.vim/packages/bundle/start/send-to-term/plugin/send-to-term.vim:

augroup sent_to_term
  autocmd BufWinEnter,TerminalOpen * if &buftype ==# 'terminal' |
        \   let t:sent_to_term_bufnr = +expand('<abuf>') |
        \ endif
augroup END

function! s:term_op(type, ...)
  let [sel, rv, rt] = [&selection, @@, getregtype('"')]
  let &selection = "inclusive"

  if a:0 
    silent exe "normal! `<" . a:type . "`>y"
  elseif a:type == 'line'
    silent exe "normal! '[V']y"
  elseif a:type == 'block'
    silent exe "normal! `[\<C-V>`]y"
    silent exe "normal! `[v`]y"

  call s:send_to_term(@@)

  let &selection = sel
  call setreg('"', rv, rt)

function! s:get_current_term(...)
  if a:0
    let bufnr = bufnr(a:1)
    let bufnr = get(t:, 'sent_to_term_bufnr', 0)
  if bufnr > 0 && bufexists(bufnr) && getbufvar(bufnr, '&buftype') ==# 'terminal'
    return bufnr
    let v:errmsg = 'No Terminal'
    throw "SendToTerm: " . v:errmsg

function! s:send_to_term(keys, ...)
  let restore = bufnr('%')
    let bufnr = call(function('s:get_current_term'), a:000)
    silent execute 'b ' . bufnr
    let lnum = line('$')
    normal! G$mp
    let keys = substitute(a:keys, '\n$', '', '')
    call term_sendkeys(bufnr, keys . "\<cr>")
    echo "Terminal: Sent " . len(keys) . " chars -> " . bufname(bufnr)
    call setpos("'[", [bufnr, count(keys, "\n") + 1 + lnum, 1, 1])
  catch /^SendToTerm:/
    echohl ErrorMsg 
    echo "Error: " . v:errmsg
    echohl None
    silent execute 'b ' . restore

function! s:goto_term(...)
    let bufnr = call(function('s:get_current_term'), a:000)
    let winid = get(win_findbuf(bufnr), 0)
    if winid > 0
      call win_gotoid(winid)
      execute 'sb ' . bufnr
  catch /^SendToTerm:/
    echohl ErrorMsg 
    echo "Error: " . v:errmsg
    echohl None

command! -range -bar -nargs=? -complete=buffer SendToTerm call s:send_to_term(join(getline(<line1>, <line2>), "\n"), <f-args>)
command! -bar GotoTerm call s:goto_term()
nmap <script> <Plug>(send-to-term-line) :<c-u>SendToTerm<cr>
nmap <script> <Plug>(send-to-term) :<c-u>set opfunc=<SID>term_op<cr>g@
xmap <script> <Plug>(send-to-term) :<c-u>call <SID>term_op(visualmode(), 1)<cr>

With this you can use :GotoTerm to jump to the last opened terminal buffer and use :SendToTerm with a range to send a range of lines to the terminal buffer. I also provided <Plug> mappings to send motions and visually selected text to the terminal.

nmap yrr <Plug>(send-to-term-line)
nmap yr <Plug>(send-to-term)
xmap R <Plug>(send-to-term)
  • Instead of adding autocmd on terminal open I currently store a buffer id of the just opened terminal into a global variable. Is there any problem with such an approach?
    – St.Antario
    Nov 16 '19 at 15:21
  • 1
    @St.Antario, nothing wrong with saving it manually. You probably don’t want all the other code I provided. Hopefully someone will find it useful. I would recommend wrapping up win_gotoid() and win_findbuf() into a command or mapping though Nov 16 '19 at 15:56

So the top buffer became "a mirror" of the terminal buffer with the number 2.

Your problem is that you confuse "buffer" and "window". Those "rectangles" you see are called "windows", while their contents are called "buffers".

So the command :b 2 says "I want a buffer number two to be shown in the current window". And that's not what you really wanted.

So just activate a terminal to type some more commands?

You have to find a terminal window and then make it active.

for w in getwininfo()
    if w.terminal
        call win_gotoid(w.winid)
  • That exe could be an exe wincmd instead
    – D. Ben Knoble
    Nov 15 '19 at 21:02
  • 1
    @D.BenKnoble win_gotoid() is even better, so I'll stick to it instead.
    – Matt
    Nov 15 '19 at 21:08
  • Nice; wonder if that's new
    – D. Ben Knoble
    Nov 15 '19 at 21:25
  • 1
    @D.BenKnoble win_gotoid() was introduced in Vim 7.4.1557 and since :terminal is the big feature of Vim 8.1 it is safe to use in this case Nov 16 '19 at 20:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.