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.
To exit the terminal, use Cntrl-D.
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 cat
ed, 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.