From 1a1a60bd0526b76ae232cc59cd1eaf5ad3ce9e77 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 14 Feb 2026 10:49:39 +0800 Subject: [PATCH] fix(terminal): resuming doesn't work with command in fish (#37857) Problem: Resuming terminal process doesn't work with command in fish. Solution: Send SIGCONT to the entire process group. Use killpg() like what bash and zsh do on `fg`: https://cgit.git.savannah.gnu.org/cgit/bash.git/tree/jobs.c?id=637f5c8696a6adc9b4519f1cd74aa78492266b7f#n3928 https://sourceforge.net/p/zsh/code/ci/77045ef899e53b9598bebc5a41db93a548a40ca6/tree/Src/jobs.c#l2674 https://sourceforge.net/p/zsh/code/ci/77045ef899e53b9598bebc5a41db93a548a40ca6/tree/Src/signals.c#l538 Install fish on CI to test this. --- .github/scripts/install_deps.sh | 4 +-- src/nvim/os/pty_proc_unix.c | 4 ++- test/functional/terminal/buffer_spec.lua | 40 ++++++++++++++++++++++++ test/functional/vimscript/eval_spec.lua | 7 ++--- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.github/scripts/install_deps.sh b/.github/scripts/install_deps.sh index dea9ec11da..ea2ceff277 100755 --- a/.github/scripts/install_deps.sh +++ b/.github/scripts/install_deps.sh @@ -31,7 +31,7 @@ if [[ $OS == Linux ]]; then fi if [[ -n $TEST ]]; then - sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb inotify-tools xdg-utils + sudo apt-get install -y locales-all cpanminus attr libattr1-dev fish gdb inotify-tools xdg-utils # Use default CC to avoid compilation problems when installing Python modules CC=cc python3 -m pip -q install --user --upgrade --break-system-packages pynvim @@ -47,7 +47,7 @@ elif [[ $OS == Darwin ]]; then brew update --quiet brew install ninja if [[ -n $TEST ]]; then - brew install cpanminus fswatch + brew install cpanminus fish fswatch npm install -g neovim npm link neovim diff --git a/src/nvim/os/pty_proc_unix.c b/src/nvim/os/pty_proc_unix.c index ad91c133bd..cc5cd899fb 100644 --- a/src/nvim/os/pty_proc_unix.c +++ b/src/nvim/os/pty_proc_unix.c @@ -241,7 +241,9 @@ void pty_proc_resize(PtyProc *ptyproc, uint16_t width, uint16_t height) void pty_proc_resume(PtyProc *ptyproc) { - kill(((Proc *)ptyproc)->pid, SIGCONT); + // Send SIGCONT to the entire process group, as some shells (e.g. fish) don't + // propagate SIGCONT to suspended child processes. + killpg(((Proc *)ptyproc)->pid, SIGCONT); } void pty_proc_close(PtyProc *ptyproc) diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 56cdaca768..9717d782cc 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -612,6 +612,46 @@ end) describe(':terminal buffer', function() before_each(clear) + it('can resume suspended PTY process running in fish', function() + skip(is_os('win'), 'N/A for Windows') + skip(fn.executable('fish') == 0, 'missing "fish" command') + + local screen = Screen.new(50, 7) + screen:add_extra_attr_ids({ + [100] = { + foreground = Screen.colors.NvimDarkGrey2, + background = Screen.colors.NvimLightGrey2, + }, + [101] = { + foreground = Screen.colors.NvimLightGrey4, + background = Screen.colors.NvimLightGrey2, + }, + [102] = { + foreground = Screen.colors.NvimDarkGrey2, + background = Screen.colors.NvimLightGrey4, + }, + }) + command('set shell=fish termguicolors') + command(('terminal %s -u NONE -i NONE'):format(fn.shellescape(nvim_prog))) + command('startinsert') + local s0 = [[ + {100:^ }| + {101:~ }|*3 + {102:[No Name] 0,0-1 All}| + {100: }| + {5:-- TERMINAL --} | + ]] + screen:expect(s0) + feed('') + screen:expect([[ + |*5 + ^[Process suspended] | + {5:-- TERMINAL --} | + ]]) + feed('') + screen:expect(s0) + end) + it('term_close() use-after-free #4393', function() command('terminal yes') feed('') -- Add input to separate two RPC requests diff --git a/test/functional/vimscript/eval_spec.lua b/test/functional/vimscript/eval_spec.lua index 49cb5cfee5..29f17fee19 100644 --- a/test/functional/vimscript/eval_spec.lua +++ b/test/functional/vimscript/eval_spec.lua @@ -24,6 +24,7 @@ local eval = n.eval local command = n.command local write_file = t.write_file local api = n.api +local fn = n.fn local sleep = vim.uv.sleep local assert_alive = n.assert_alive local poke_eventloop = n.poke_eventloop @@ -88,10 +89,8 @@ describe('backtick expansion', function() end) it('with shell=fish', function() - if eval("executable('fish')") == 0 then - pending('missing "fish" command') - return - end + t.skip(fn.executable('fish') == 0, 'missing "fish" command') + command('set shell=fish') command(':silent args `echo ***2`') eq({ 'file2' }, eval('argv()'))