2

If I start typing an external command such as :!gi and then press Tab, vim will autocomplete the command name ("git"). But if I enter :!git comm and press Tab, I get no completions - vim doesn't provide tab completion for anything except the name ("git") of the command itself.

If I enter git comm in Bash and press Tab, I will get completion for all of git's subcommands, options, etc.

Is there a way to get this level of command completion for running external commands from vim? (To be clear I'm looking for a general solution, not just for the git command).

1 Answer 1

1

Unfortunately, Bash doesn't easily expose its programmable completion functionality to be consumed by other programs. But if you're ok with a rather ugly looking solution, using the following in my .vimrc has been working very well for me:

Note the hardcoded path source /usr/share/bash-completion/bash_completion in the shell script - this works on Fedora, but it may be different for other distros. Check your /etc/profile to see where it gets sourced from.

command! -nargs=+ -complete=custom,s:BashCmdComp ExtCmd :!<args>
cnoremap ! <c-r>=getcmdpos() == 1 ? "ExtCmd " : "!"<cr>

func s:BashCmdComp(arglead, vimcmdline, vimcursorpos)
    " strip vim command
    let l:cmdline = substitute(a:vimcmdline, '^\S\+ \(.*\)', '\1', "g")
    let l:cursorpos = 1 + a:vimcursorpos - (strlen(a:vimcmdline) - strlen(l:cmdline))

    " remove everything after the cursor so that completion can always act on the last word
    let l:cmdline = l:cmdline[:l:cursorpos]

    if count(l:cmdline, " ") == 0
        return system("compgen -A command " . a:arglead)
    end

    let l:cmdname = substitute(l:cmdline, '^\(\S\+\) .*$', '\1', "")
    let l:scriptlines =<< trim eval END
        __print_completions() {{
            printf '%s\n' "${{COMPREPLY[@]% }}"
        }}

        # This is the path on Fedora, it may be different for other distros
        source /usr/share/bash-completion/bash_completion
        _completion_loader {l:cmdname}

        COMP_WORDS=({l:cmdline})
        COMP_LINE='{l:cmdline}'
        COMP_POINT={l:cursorpos}
        COMP_CWORD=$((${{#COMP_WORDS[@]}} - 1))
        [ $COMP_CWORD -eq 0 ] && COMP_CWORD=1
        $(complete -p {l:cmdname} | sed 's/^.*-F \([^ ][^ ]*\).*/\1/')
        __print_completions
    END

    let l:output = trim(system(join(l:scriptlines, "\n")))
    if l:output != ""
        return l:output
    else
        return join(getcompletion(a:arglead, "file"), "\n")
    endif
endfunc
1
  • Some explanation of the shell in the answer would improve it
    – D. Ben Knoble
    Commented Oct 21 at 16:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.