From cf2d8995001bf466975e79409cb5e9cf632526ed Mon Sep 17 00:00:00 2001 From: Jan Geboers Date: Wed, 18 Feb 2026 16:40:58 +0100 Subject: [PATCH] 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 --- plug.vim | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/plug.vim b/plug.vim index f479fba..6a45379 100644 --- a/plug.vim +++ b/plug.vim @@ -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(':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