Use job API for system() calls in GUI Vim to avoid overhead

In GUI Vim (gvim, MacVim), each system() call incurs significant
overhead due to command prompt window creation, making operations
like PlugDiff ~5x slower than in terminal Vim or Neovim.

Add s:system_job() which uses job_start with file-based output to
run commands without that overhead, and use it in s:system() when
running in GUI Vim with job support.

Since v:shell_error is read-only in Vim 9, introduce s:shell_error
which is set by both code paths and used at all call sites.

Fixes #1312
This commit is contained in:
Jan Geboers 2026-02-18 16:40:58 +01:00
parent d72ac8734f
commit cf2d899500

View file

@ -68,6 +68,7 @@ let s:mac_gui = has('gui_macvim') && has('gui_running')
let s:is_win = has('win32')
let s:nvim = has('nvim-0.2') || (has('nvim') && exists('*jobwait') && !s:is_win)
let s:vim8 = has('patch-8.0.0039') && exists('*job_start')
let s:shell_error = 0
if s:is_win && &shellslash
set noshellslash
let s:me = resolve(expand('<sfile>:p'))
@ -170,7 +171,7 @@ function! s:git_origin_branch(spec)
" The command may not return the name of a branch in detached HEAD state
let result = s:lines(s:system('git symbolic-ref --short HEAD', a:spec.dir))
return v:shell_error ? '' : result[-1]
return s:shell_error ? '' : result[-1]
endfunction
if s:is_win
@ -1097,7 +1098,7 @@ function! s:checkout(spec)
let credential_helper = s:disable_credential_helper() ? '-c credential.helper= ' : ''
let output = s:system(
\ 'git '.credential_helper.'fetch --depth 999999 && git checkout '.plug#shellescape(sha).' --', a:spec.dir)
let error = v:shell_error
let error = s:shell_error
endif
return [output, error]
endfunction
@ -1303,7 +1304,7 @@ function! s:update_finish()
let tag = spec.tag
if tag =~ '\*'
let tags = s:lines(s:system('git tag --list '.plug#shellescape(tag).' --sort -version:refname 2>&1', spec.dir))
if !v:shell_error && !empty(tags)
if !s:shell_error && !empty(tags)
let tag = tags[0]
call s:log4(name, printf('Latest tag for %s -> %s', spec.tag, tag))
call append(3, '')
@ -1311,7 +1312,7 @@ function! s:update_finish()
endif
call s:log4(name, 'Checking out '.tag)
let out = s:system('git checkout -q '.plug#shellescape(tag).' -- 2>&1', spec.dir)
let error = v:shell_error
let error = s:shell_error
endif
if !error && filereadable(spec.dir.'/.gitmodules') &&
\ (s:update.force || has_key(s:update.new, name) || s:is_updated(spec.dir))
@ -1319,7 +1320,7 @@ function! s:update_finish()
let out .= s:bang('git submodule update --init --recursive'.s:submodule_opt.' 2>&1', spec.dir)
let error = v:shell_error
endif
let msg = s:format_message(v:shell_error ? 'x': '-', name, out)
let msg = s:format_message(error ? 'x': '-', name, out)
if error
call add(s:update.errors, name)
call s:regress_bar()
@ -1480,7 +1481,7 @@ function! s:spawn(name, spec, queue, opts)
endif
else
let job.lines = s:lines(call('s:system', has_key(a:opts, 'dir') ? [argv, a:opts.dir] : [argv]))
let job.error = v:shell_error != 0
let job.error = s:shell_error != 0
let job.running = 0
endif
endfunction
@ -2334,6 +2335,21 @@ function! s:with_cd(cmd, dir, ...)
return printf('%s %s %s %s', cd, plug#shellescape(a:dir, {'script': script, 'shell': &shell}), sep, a:cmd)
endfunction
function! s:system_job(cmd) abort
let tmp = tempname()
let job = job_start(['/bin/sh', '-c', a:cmd], {
\ 'out_io': 'file',
\ 'out_name': tmp,
\ 'err_io': 'out',
\})
while job_status(job) ==# 'run'
endwhile
let s:shell_error = job_info(job).exitval
let result = filereadable(tmp) ? join(readfile(tmp, 'b'), "\n") : ''
silent! call delete(tmp)
return result
endfunction
function! s:system(cmd, ...)
let batchfile = ''
try
@ -2343,7 +2359,9 @@ function! s:system(cmd, ...)
" but it cannot set the working directory for the command.
" Assume that the command does not rely on the shell.
if has('nvim') && a:0 == 0
return system(a:cmd)
let ret = system(a:cmd)
let s:shell_error = v:shell_error
return ret
endif
let cmd = join(map(copy(a:cmd), 'plug#shellescape(v:val, {"shell": &shell, "script": 0})'))
if s:is_powershell(&shell)
@ -2358,7 +2376,12 @@ function! s:system(cmd, ...)
if s:is_win && type(a:cmd) != s:TYPE.list
let [batchfile, cmd] = s:batchfile(cmd)
endif
return system(cmd)
if s:vim8 && has('gui_running') && !s:is_win
return s:system_job(cmd)
endif
let ret = system(cmd)
let s:shell_error = v:shell_error
return ret
finally
let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd]
if s:is_win && filereadable(batchfile)
@ -2369,7 +2392,7 @@ endfunction
function! s:system_chomp(...)
let ret = call('s:system', a:000)
return v:shell_error ? '' : substitute(ret, '\n$', '', '')
return s:shell_error ? '' : substitute(ret, '\n$', '', '')
endfunction
function! s:git_validate(spec, check_branch)
@ -2412,7 +2435,7 @@ function! s:git_validate(spec, check_branch)
\ 'git', 'rev-list', '--count', '--left-right',
\ printf('HEAD...origin/%s', origin_branch)
\ ], a:spec.dir)), '\t')
if v:shell_error || len(ahead_behind) != 2
if s:shell_error || len(ahead_behind) != 2
let err = "Failed to compare with the origin. The default branch might have changed.\nPlugClean required."
else
let [ahead, behind] = ahead_behind
@ -2562,7 +2585,7 @@ function! s:upgrade()
try
let out = s:system(['git', 'clone', '--depth', '1', s:plug_src, tmp])
if v:shell_error
if s:shell_error
return s:err('Error upgrading vim-plug: '. out)
endif