3

I've traditionally used vim as a text editor and tmux as a terminal multiplexer, but I'm trying to see if I can drop tmux entirely and just use vim for everything (except session attach/detach, which will be handled by an external tool). So far, I have almost zero knowledge about terminal emulators in vim and how to customize them.

My main question is: should I base this setup upon neovim or vim 8? From what I gathered, terminal support is largely different in those 2 programs. I know that neovim implementation should be more mature, since it got terminal first, but I also noticed that terminal help in neovim is very sparse, while help page in vim 8 is much more expansive.

My second question is whether that's at all feasible.

Here are some of the functionality I'm hoping to achieve (don't yet know if all of this is possible):

  • Open new splits with terminal similar to tmux (each split is a separate terminal)
  • Ability to go into netrw from terminal with working directory being one opened in shell
  • Start terminal in currently from directory currently open in netrw
  • Have separate working directory for each tab/split
  • Edit terminal command in normal mode without opening nested editor with C-x C-e
1

should I base this setup upon neovim or vim 8?

If you don't have any problems with Vim, there's no need to switch to Neovim. In particular, Vim's terminal support is very decent.

Open new splits with terminal similar to tmux (each split is a separate terminal)

Yes, it's trivial. In fact, it's hard to avoid this.

Ability to go into netrw from terminal with working directory being one opened in shell

First, netrw sucks. Seriously consider migrating to NERDTree or vim-dirvish (or maybe vim-vinegar).

Second, that requires Vim to get CWD of another process. In general, that could be tricky, but as for shell you probably can live with term_gettitle().

Start terminal in currently from directory currently open in netrw

That's not about terminal support, but rather about netrw/other plugin. Easily done with dirvish. Perhaps, the same for others.

Have separate working directory for each tab/split

Again, that has nothing to do with terminal support (every process has CWD of its own anyway). But every window (and tab) in Vim can also have CWD of its own (see :h lcd).

Edit terminal command in normal mode without opening nested editor with C-x C-e

Vim (and also Neovim) has Normal mode for terminal window, but it's read-only. It should be possible to do some tricks with tnoremap and term_sendkeys(), but that will not be true editing mode either. Bash setting set -o vi should be much more practical.

4
  • 2
    *netrw sucks*—meh, that’s your opinion. I like it. It has advantages and disadvantages, like everything... great answer ‘til I read that though...
    – D. Ben Knoble
    Sep 27 '19 at 23:38
  • 1
    @D.BenKnoble Maybe that sounded a bit harsh, but I became too frustrated when attempted to use it. And although all "file managers" for Vim surely have some flaws and a "standard" plugin should normally be preferred, but I still ended switching to dirvish which, IMO, is a lesser evil. The support level of netrw is awful. Why, they even haven't fixed the bug which prevents going to the drive's rootdir under Windows for years, and you have to apply the patch manually. Custom scripting for netrw is also very inconvenient, especially compared with a "regular buffer" model of vinegar/dirvish.
    – Matt
    Sep 28 '19 at 7:50
  • vim-vinegar is just a light wrapper around netrw with a couple key mappings (notably -) and the help text disabled. I recommend using vinegar and reading the README to understand the philosophy and why you should try it before you use NERDTree. May 15 at 2:28
  • @JimStewart Personally I like more the one that I write myself.
    – Matt
    May 15 at 7:37
0

With a few mappings, vim becomes an excellent terminal multiplexer, and tmux becomes unnecessary. In addition to the features you have listed (which are all possible with tmux), using terminal buffers directly inside (neo)vim provides some additional features tmux can't match:

  • completion inside e.g. the git commit message buffer can autocomplete terminal commands / output
  • you can map vim keybindings to grab information such as the current filename or word under the cursor, switch to the terminal, and issue some command
  • you can easily hide and re-find closed hidden buffers, so that you can run some long-running command and not be bothered with an extra window in the way
  • you can change the vim working directory to that of the terminal, or vice-versa

I have gotten by for many years with just these mappings in my vimrc:

tnoremap <ESC> <C-\><C-n>
nnoremap <Leader>cc :term<CR>A
nnoremap <Leader>cs :split<CR>:wincmd j<CR>:term<CR>A
nnoremap <Leader>ct :tabnew<CR>:term<CR>A

The mnemonics are; command-window, split, tab. If you need to work on windows as well, check out my vimrc's terminal.vim file, which calls git-bash by default, but offers dos mnemonic keybinding as well for when you need a DOS prompt.

You will also want a convenient way to exit the terminal again:

augroup TerminalMappings
  autocmd!
  autocmd TermOpen * nnoremap <buffer> <C-E> aexit<CR>
  autocmd TermOpen * tnoremap <buffer> <C-E> exit<CR>
augroup END

Now, you want to be able to switch to the directory of the terminal, so that you can subsequently do :e to open netrw / dirvish. This one is a bit more complicated that you may need since it also caters to windows, but I'm not going to edit that out; you may need that one day:

fu! GetDirFromPrompt() abort
  let l:line=getline('.')
  if l:line =~ '^[^> ]*@[^> ]* MINGW.. '
    if has('win32') || has ('win64')
      " USER@DOMAIN MINGW64 ~/vimscripts/dein/repos/github.com/autozimu/LanguageClient-neovim_next (next)
      let home='/' .. $HOME[0] .. substitute($HOME[2:], '\', '/', 'g')
      let l:line=substitute(l:line, '\~', home, '')
    endif
    " USER@DOMAIN MINGW64 /c/code/with spaces
    " USER@DOMAIN MINGW64 /c/code/in_git (master)
    let dir=substitute(substitute(substitute(l:line, '.*MINGW.. /\(.\)', '\1:', ''), '(.*)$', '', ''), '/', '\', 'g')
  elseif l:line =~ '.:[^>]*>.*'
    " C:\Program Files\Neovim\bin>some user-input
    let dir=substitute(l:line, '>.*', '', '')
  elseif l:line =~ '^[^@> ]*@[^:>@ ]*:[^$]'
    " tama@tama-hp-laptop:~/code/adacore/libadalang$
    let dir=substitute(substitute(l:line, '$.*', '', ''), '^[^@> ]*@[^:>@ ]*:', '', '')
  else
    throw 'No pattern matches '.l:line
  endif
  return dir
endfunction

" Change directory to current line
fu! DirToCurrentLine() abort
  if &buftype ==# 'terminal'
    let l:dir=GetDirFromPrompt()
  else
    let l:dir=expand('%:p:h')
  endif
  exe 'cd '.l:dir
  echom 'cd '.l:dir
endfunction

That function will read the prompt when in a terminal buffer, and change vim's directory to that. But, if in a non-terminal buffer, it just changes directory to that of the current file. I also recommend mapping fugitive's :Gcd to something handy, I use that a lot just prior to opening a terminal split. Recommended mapping:

nnoremap <leader>qq :call DirToCurrentLine()<CR>
nnoremap <leader>qg :Gcd<CR>

Finally, you want to be able to move your cursor onto a prompt line in the terminal, and be able to immediately open that directory. Note that this mapping is assuming that you have a terminal split below; when the terminal is fullscreen (because you used ct) then instead it will replace the terminal window, which becomes hidden and can later be recalled using e.g. fzf.vim's :Buffers.

function! BrowseDir() abort
  if &buftype ==# 'terminal'
    let l:dir = GetDirFromPrompt()
    " switch to buffer above
    silent execute 'wincmd k'
    execute ':e '.l:dir
  else
    e %:h
  endif
endfunction

" open directory of current file / terminal prompt
nnoremap - :call BrowseDir()<CR>

Again, the function conceptually does the same but is implemented differently for a terminal buffer, where it grabs the prompt line and extracts the directory, than for a regular file, whose directory it opens immediately.

Having defined all that, your use-cases are done like this:

  • Open new splits with terminal similar to tmux -> <Leader>cs
  • Ability to go into netrw from terminal -> -
  • Start terminal in dir currently open in netrw -> <Leader>qq followed by <Leader>cs
  • Have separate dir for each tab/split -> :tabnew, :lcd some/dir, <Leader>cc

Only your last one doesn't really work:

  • Edit terminal command without opening nested editor

You can set your editor to neovim remote, so that if you issue e.g. git commit then a new buffer is opened in the current vim instance, instead of vim in vim, which for me solves the problem, but I'm not sure if that is your use case.

I have added this to my bashrc to set the nvr editor only for the terminal embedded inside vim, not every terminal window:

if [[ $VIMEMBEDDEDTERMINAL ]]; then
  export EDITOR='nvr'
  export VISUAL='nvr'
else
  export VIMEMBEDDEDTERMINAL=true
  export EDITOR='nvim'
  export VISUAL='nvim'
fi

But you can go beyond what is possible in tmux with this setup; for instance, to grab the current filename and do some magic command on it:

function! UseAbsoluteFilenameInTermBelow(prefix, ...) abort
  let l:postfix = get(a:, 1, '')
  let l:filename = expand('%:p')
  " switch to bottom terminal buffer
  silent execute 'wincmd j'
  call feedkeys("a" . a:prefix . l:filename . l:postfix . "\<CR>")
endfunction

function! UseRelativeFilenameInTermBelow(prefix, ...) abort
    let l:postfix = get(a:, 1, '')
    let l:filename = bufname('%')
    " switch to bottom terminal buffer
    silent execute 'wincmd j'
    call feedkeys("a" . a:prefix . l:filename . l:postfix . "\<CR>")
endfunction

Note the need for feedkeys when inserting commands into a terminal buffer from inside a vimscript function.

For example, you could map a keybinding to :call UseRelativeFilenameInTermBelow('clang++ -Weverything'). Then, if it returns errors, you want a way to jump up again and navigate to that exact file, line, and column; although you can also use :make and the quickfix list for this, I find that being able to open directly from the terminal's output is a lot more versatile; the error might be from a server-generated logfile which I happened to have cated, or from ffgrep (defined as ionice -c3 find . -type f | xargs -I{} grep -n -i "$1" "{}" /dev/null ) or from any other terminal command which outputs filenames and optional line and column numbers. I implemented that in open_file_in_top_buffer.vim:

" Open file at position from compiler error on the terminal
" e.g. foobar.adb:27:2: "X" not declared in "Y"
" results in opening foobar.adb in the top buffer (not the terminal), and issuing '27G2|'
" Default vim comes close with 'vt:<C-W>gf' - but:
" 1) including the [colon][linenumber] suffix does not work as intended in NeoVim
" 2) this does not include the column, 
" 3) you cannot reuse the top window.

" TODO strip common parts of current path so that when I am somewhere deeper
" in my project, I can still gf to a file path specified from the root of that
" project
fu! OpenfileInTopBuffer(s)
  let selection=a:s
  if selection[0]=='"'
    let selection=selection[1:]
  endif
  if selection[-1]==','
    let selection=selection[:-1]
  endif
  if selection[-1]=='"'
    let selection=selection[:-1]
  endif
  if selection[1:1]==':'
    " One letter directory assumed to be drivename under windows
    let elements=split(selection[2:], ':')
    let elements[0]=selection[0:1]..elements[0]
  else
    let elements=split(selection, ':')
  endif
  let elementlen=len(elements)
  let filename=elements[0]
  if matchstr(filename, "^.*\\..*$")=="" " doesn't look like filename (. missing)
    if input("Really open "..filename.."? (y/n)")!="y"
      return
    endif
  endif
  if elementlen > 1
    let line=elements[1]
    if matchstr(line, "^[0-9]*$")=="" " line is not a number
      let elementlen=1
    endif
    if elementlen > 2
      let column=elements[2]
      if matchstr(column, "^[0-9]*$")=="" " column is not a number
        let elementlen=2
      endif
    endif
  endif
  " switch to top buffer
  silent execute 'wincmd k'
  " get rid of localdir if present
  if haslocaldir()
    execute 'cd' getcwd(-1)
  endif
  try
    " find the file 
    if elementlen > 1
      " keepjumps ensures the top of the file is not added to the jumplist
      silent execute 'keepjumps find ' . filename
    else
      silent execute 'find ' . filename
      return
    endif
    if elementlen >= 3
      " go to the indicated line and column
      silent execute 'normal! ' . line . 'G' . column . '|'
    else " elementlen == 2
      " go to the indicated line
      silent execute 'normal! ' . line . 'G'
    endif
  endtry
endfunction

function! GetVisualSelection()
  let reg = '"'
  let [save_reg, save_type] = [getreg(reg), getregtype(reg)]
  normal! gv""y
  let text = @"
  call setreg(reg, save_reg, save_type)
  return text
endfunction

augroup Terminal_gf_mapping
  autocmd!
  autocmd TermOpen * nnoremap <silent> <buffer> gf :call OpenfileInTopBuffer( expand('<cWORD>') )<CR>
augroup END

vnoremap <silent> gf :call OpenfileInTopBuffer( GetVisualSelection() )<CR>

" with a capital is to create if it doesn't exist yet
noremap gF :e <cfile><cr>

The visual mode mapping helps when my vim current working directory is deeper than the dir in which the command was run; I can just visually select the filename, chomping off the prefix which isn't relevant, and do gf. Also note that this overrides the default vim gf, and conceptually does the same thing.

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.