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 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.