1

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?

2 Answers 2

3

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!
  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"
  else
    silent exe "normal! `[v`]y"
  endif

  call s:send_to_term(@@)

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

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

function! s:send_to_term(keys, ...)
  let restore = bufnr('%')
  try
    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
  finally
    silent execute 'b ' . restore
  endtry
endfunction

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

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)
2
  • 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, 2019 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, 2019 at 15:56
3

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)
        break
    endif
endfor
4
  • That exe could be an exe wincmd instead
    – D. Ben Knoble
    Nov 15, 2019 at 21:02
  • 1
    @D.BenKnoble win_gotoid() is even better, so I'll stick to it instead.
    – Matt
    Nov 15, 2019 at 21:08
  • Nice; wonder if that's new
    – D. Ben Knoble
    Nov 15, 2019 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, 2019 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.