Skip to content

Instantly share code, notes, and snippets.

@adscriven
Last active May 30, 2018 11:17
Show Gist options
  • Save adscriven/1fde7e3aa362b8f9700677352e97f56e to your computer and use it in GitHub Desktop.
Save adscriven/1fde7e3aa362b8f9700677352e97f56e to your computer and use it in GitHub Desktop.
Edit files under the cursor in :terminal
" termedit.vim -- Edit a file under the cursor in a terminal window.
" https://gist.github.com/adscriven/1fde7e3aa362b8f9700677352e97f56e
" Public domain.
" [ ] add mappings for opening a terminal window and doing 'ls'
" REQUIREMENTS
" Vim 8.0.0693 or later with the +terminal feature.
" Supports bash with both vi and emacs keybindings, and cmd.exe.
" INSTALLATION
" Chuck this file in ~/.vim/plugin.
" USAGE
" In your vimrc,
" nmap {mykey} <Plug>[termedit]
" nmap {myotherkey} <Plug>[termsplit]
" DESCRIPTION
" * After e.g. ls or grep in a terminal window, place the cursor on
" a filename and press {mykey} to edit the file.
" * If the filename is immediately followed by a colon then
" a number, it will be treated as a line number to jump to.
" * Alternatively, type a file name at the prompt, switch to
" Terminal-Normal mode, then press {mykey} to edit it.
" * The mappings will do nothing if the cursor is not in a terminal
" buffer.
" * Note that :e! is used to edit the file for the <Plug>[termedit]
" mapping. The terminal window will be replaced with the window of
" the edited file, and the terminal job will be unceremoniously
" terminated.
" TIPS
" * To conditionally map e.g. <CR> only for a terminal buffer:
" augroup vimrc_terminal
" autocmd!
" autocmd TerminalOpen *
" \ nmap <buffer> <CR> <Plug>[termedit]|
" \ nmap <buffer> \<CR> <Plug>[termsplit]
" augroup END
" For Vim 8.0 use "autocmd BufWinEnter * if &buftype == 'terminal' | ..."
" instead.
"
" * Make it easy to open a terminal window with a directory listing
" already there.
"
" nno <silent> \1 :<c-u>exe 'term ++curwin' \| call feedkeys("ls\<lt>cr>")<cr>
" nno <silent> \2 :<c-u>exe 'term' \| call feedkeys("ls\<lt>cr>")<cr>
"
" * Make it easy to switch to Terminal-Normal mode:
" tnoremap <Esc> <C-W>N
" or whatever key combination you normally use to exit to Normal
" mode.
"
" Combining the above three tips gives a capable directory browser.
" KNOWN ISSUES
" 1. `:e!`, or `:hide e`? Should the terminal job be automatically
" killed or not?
" 2. MSYS Vim is particularly slow. term_wait() may be needed to
" prevent erratic results.
" 3. s:send() needs work to make sure it operates correctly with
" various shells and OSs.
" 4. Doesn't handles [nnn]c:\foo\bar.txt style line numbers for
" cmd.exe. ':' is considered to be a valid file-name character
" on Windows.
if !has('terminal')
echohl errormsg
echomsg '(termedit.vim) Vim 8.0.0693 or later with +terminal ' .
\ 'is required. Exiting.'
echohl None
finish
endif
let s:cpo = &cpo
set cpo&vim
let s:shell = matchstr(&shell, '[^/\\]\+$')
let s:clearline = {
\ 'bash': "\<esc>S\<c-k>\<c-u>",
\ 'bash.exe': "\<esc>S\<c-k>\<c-u>",
\ 'cmd.exe': "\<esc>\<esc>"
\ }[s:shell]
let s:cwdcmd = {
\ 'bash': 'pwd',
\ 'bash.exe': 'pwd',
\ 'cmd.exe': 'cd'
\ }[s:shell]
let s:outputlinedelim = {
\ 'bash': "\r\n",
\ 'bash.exe': "\r\n",
\ 'cmd.exe': "\r\n"
\ }[s:shell]
" Send a string, s, to the job for the terminal of the current buffer.
" Split the response on \r\n, and remove the first item of the resulting
" list (s echoed), and the last item (terminal prompt). Return the
" trimmed list.
" First sends a key sequence to clear the terminal input line.
" XXX: Maybe term_gettitle() could be used to get the cwd. Possibly
" not reliable, plus this won't work for cmd.exe.
let s:send = {s ->
\ split(ch_evalraw(term_getjob(bufnr('%')), s:clearline . s . "\<cr>"),
\ s:outputlinedelim)[1:-2]}
" Return the cwd of the terminal in the current buffer.
" Trims text from Esc onwards, to deal with cmd.exe output.
let s:pwd = {-> substitute(get(s:send(s:cwdcmd), 0, ''), '\e.*', '', '')}
" Return the filename under the cursor. Prepend with the result of
" s:pwd() for relative paths. For paths starting with '/' (Unix)
" or letter-colon (Windows) leave the filename untouched.
let s:cursorfile = {-> {s -> s == ''
\ ? ''
\ : s[0] == '/' || s[0:1] =~ '^\a:' ? s : s:pwd() . '/' . s
\ }(substitute(expand('<cfile>'), ':\d\+', '', ''))}
" Return a line number that may be following the filename, e.g. grep
" output such as
" foo.c:123:/* Behold awesome code! */
" If no line number is found, return an empty string.
let s:cursorfilelnum = {->
\ matchstr(expand('<cWORD>'), ':\zs\d\+')}
" Return an ex command to edit a file, maybe jumping to a line number.
let s:editcmd = {prefix -> {lnum -> {file -> file == ''
\ ? ''
\ : prefix . (lnum ? '+' . lnum : '') . ' ' . file
\ }(s:cursorfile())}(s:cursorfilelnum())}
fun! s:mapping(prefix)
if &buftype != 'terminal'
return ''
endif
return ":\<c-u>" . s:editcmd(a:prefix) . "\<cr>"
endfun
nnoremap <expr> <Plug>[termedit] <SID>mapping('e!')
nnoremap <expr> <Plug>[termsplit] <SID>mapping('sp')
let &cpo = s:cpo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment