5

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
2
  • "neovim or vim8" it depends: how much time do you spend editing files on remote machines? For me when it was "all the time" I used vim, and now that it rounds to "never" the answer is neovim. There was never any question other than "do I need to be able to get quickly up and running on a given Linux box", because neovim is better in about every possible way except that one, where it's arguably much much worse. For what you're doing though there really isn't that much difference IMO. Commented Dec 18, 2022 at 12:03
  • This is a deal breaker for using either vim or neovim as terminal: github.com/vim/vim/pull/8365 Commented Dec 20, 2022 at 12:27

2 Answers 2

2

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... Commented Sep 27, 2019 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. Commented Sep 28, 2019 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. Commented May 15, 2021 at 2:28
  • @JimStewart Personally I like more the one that I write myself. Commented May 15, 2021 at 7:37
1

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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.