diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 3ae225dc66..1540ebba5e 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -382,6 +382,8 @@ TERMINAL • |nvim_open_term()| can be called with a non-empty buffer. The buffer contents are piped to the PTY and displayed as terminal output. • CSI 3 J (the sequence to clear terminal scrollback) is now supported. +• A suspended PTY process is now indicated by "[Process suspended]" at the + bottom-left of the buffer and can be resumed by pressing a key. TREESITTER diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index f5f8ade7e9..c753463699 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1144,6 +1144,7 @@ Integer nvim_open_term(Buffer buffer, Dict(open_term) *opts, Error *err) .height = (uint16_t)curwin->w_view_height, .write_cb = term_write, .resize_cb = term_resize, + .resume_cb = term_resume, .close_cb = term_close, .force_crlf = GET_BOOL_OR_TRUE(opts, open_term, force_crlf), }; @@ -1194,6 +1195,10 @@ static void term_resize(uint16_t width, uint16_t height, void *data) // TODO(bfredl): Lua callback } +static void term_resume(void *data) +{ +} + static void term_close(void *data) { Channel *chan = data; diff --git a/src/nvim/channel.c b/src/nvim/channel.c index 1f9aebc941..c8fbee1c24 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -393,6 +393,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s proc->argv = argv; proc->exepath = exepath; proc->cb = channel_proc_exit_cb; + proc->state_cb = channel_proc_state_cb; proc->events = chan->events; proc->detach = detach; proc->cwd = cwd; @@ -762,6 +763,14 @@ static void channel_proc_exit_cb(Proc *proc, int status, void *data) channel_decref(chan); } +static void channel_proc_state_cb(Proc *proc, bool suspended, void *data) +{ + Channel *chan = data; + if (chan->term) { + terminal_set_state(chan->term, suspended); + } +} + static void channel_callback_call(Channel *chan, CallbackReader *reader) { Callback *cb; @@ -812,6 +821,7 @@ void channel_terminal_alloc(buf_T *buf, Channel *chan) .height = chan->stream.pty.height, .write_cb = term_write, .resize_cb = term_resize, + .resume_cb = term_resume, .close_cb = term_close, .force_crlf = false, }; @@ -839,6 +849,14 @@ static void term_resize(uint16_t width, uint16_t height, void *data) pty_proc_resize(&chan->stream.pty, width, height); } +static void term_resume(void *data) +{ +#ifdef UNIX + Channel *chan = data; + pty_proc_resume(&chan->stream.pty); +#endif +} + static inline void term_delayed_free(void **argv) { Channel *chan = argv[0]; diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index 9e1ac24526..d1080958af 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -1441,6 +1441,17 @@ static void win_update(win_T *wp) decor_providers_invoke_win(wp); + if (buf->terminal && terminal_suspended(buf->terminal)) { + static VirtTextChunk chunk = { .text = "[Process suspended]", .hl_id = -1 }; + static DecorVirtText virt_text = { + .priority = DECOR_PRIORITY_BASE, + .pos = kVPosWinCol, + .data.virt_text = { .items = &chunk, .size = 1 }, + }; + decor_range_add_virt(&decor_state, buf->b_ml.ml_line_count - 1, 0, + buf->b_ml.ml_line_count - 1, 0, &virt_text, false); + } + FOR_ALL_WINDOWS_IN_TAB(win, curtab) { if (win->w_buffer == wp->w_buffer && win_redraw_signcols(win)) { changed_line_abv_curs_win(win); diff --git a/src/nvim/event/defs.h b/src/nvim/event/defs.h index 3309c934b5..6d322b203e 100644 --- a/src/nvim/event/defs.h +++ b/src/nvim/event/defs.h @@ -152,6 +152,7 @@ typedef enum { /// OS process typedef struct proc Proc; typedef void (*proc_exit_cb)(Proc *proc, int status, void *data); +typedef void (*proc_state_cb)(Proc *proc, bool suspended, void *data); typedef void (*internal_proc_cb)(Proc *proc); struct proc { @@ -169,6 +170,7 @@ struct proc { RStream out, err; /// Exit handler. If set, user must call proc_free(). proc_exit_cb cb; + proc_state_cb state_cb; internal_proc_cb internal_exit_cb, internal_close_cb; bool closed, detach, overlapped, fwd_err; MultiQueue *events; diff --git a/src/nvim/event/proc.h b/src/nvim/event/proc.h index 50aee57d85..94efaf3a4c 100644 --- a/src/nvim/event/proc.h +++ b/src/nvim/event/proc.h @@ -25,6 +25,7 @@ static inline Proc proc_init(Loop *loop, ProcType type, void *data) .out = { .s.closed = false, .s.fd = STDOUT_FILENO }, .err = { .s.closed = false, .s.fd = STDERR_FILENO }, .cb = NULL, + .state_cb = NULL, .closed = false, .internal_close_cb = NULL, .internal_exit_cb = NULL, diff --git a/src/nvim/os/pty_proc_unix.c b/src/nvim/os/pty_proc_unix.c index 5c7f68a432..ad91c133bd 100644 --- a/src/nvim/os/pty_proc_unix.c +++ b/src/nvim/os/pty_proc_unix.c @@ -239,6 +239,11 @@ void pty_proc_resize(PtyProc *ptyproc, uint16_t width, uint16_t height) ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize); } +void pty_proc_resume(PtyProc *ptyproc) +{ + kill(((Proc *)ptyproc)->pid, SIGCONT); +} + void pty_proc_close(PtyProc *ptyproc) FUNC_ATTR_NONNULL_ALL { @@ -397,13 +402,22 @@ static void chld_handler(uv_signal_t *handle, int signum) for (size_t i = 0; i < kv_size(loop->children); i++) { Proc *proc = kv_A(loop->children, i); do { - pid = waitpid(proc->pid, &stat, WNOHANG); + pid = waitpid(proc->pid, &stat, WNOHANG|WUNTRACED|WCONTINUED); } while (pid < 0 && errno == EINTR); if (pid <= 0) { continue; } + if (WIFSTOPPED(stat)) { + proc->state_cb(proc, true, proc->data); + continue; + } + if (WIFCONTINUED(stat)) { + proc->state_cb(proc, false, proc->data); + continue; + } + if (WIFEXITED(stat)) { proc->status = WEXITSTATUS(stat); } else if (WIFSIGNALED(stat)) { diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 41408ef36a..e305c2a566 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -174,6 +174,8 @@ struct terminal { bool in_altscreen; // program exited bool closed; + // program suspended + bool suspended; // when true, the terminal's destruction is already enqueued. bool destroy; @@ -672,7 +674,6 @@ void terminal_close(Terminal **termpp, int status) // only need to call the close callback to clean up the terminal object. only_destroy = true; } else { - term->forward_mouse = false; // flush any pending changes to the buffer if (!exiting) { block_autocmds(); @@ -726,7 +727,33 @@ void terminal_close(Terminal **termpp, int status) } } +static void terminal_state_change_event(void **argv) +{ + handle_T buf_handle = (handle_T)(intptr_t)argv[0]; + buf_T *buf = handle_get_buffer(buf_handle); + if (buf && buf->terminal) { + // Don't change the actual terminal content to indicate the suspended state here, + // as unlike the process exit case the change needs to be reversed on resume. + // Instead, the code in win_update() will add a "[Process suspended]" virtual text + // at the botton-left of the buffer. + redraw_buf_line_later(buf, buf->b_ml.ml_line_count, false); + } +} + +/// Updates the suspended state of the terminal program. +void terminal_set_state(Terminal *term, bool suspended) + FUNC_ATTR_NONNULL_ALL +{ + if (term->suspended != suspended) { + // Trigger a main loop iteration to redraw the buffer. + multiqueue_put(main_loop.events, terminal_state_change_event, + (void *)(intptr_t)term->buf_handle); + } + term->suspended = suspended; +} + void terminal_check_size(Terminal *term) + FUNC_ATTR_NONNULL_ALL { if (term->closed) { return; @@ -951,9 +978,14 @@ static void terminal_check_cursor(void) if (topline != curwin->w_topline) { set_topline(curwin, topline); } - // Nudge cursor when returning to normal-mode. - int off = is_focused(term) ? 0 : (curwin->w_p_rl ? 1 : -1); - coladvance(curwin, MAX(0, term->cursor.col + off)); + if (term->suspended) { + // If the terminal process is suspended, keep cursor at the bottom-left corner. + curwin->w_cursor = (pos_T){ .lnum = curbuf->b_ml.ml_line_count }; + } else { + // Nudge cursor when returning to normal-mode. + int off = is_focused(term) ? 0 : (curwin->w_p_rl ? 1 : -1); + coladvance(curwin, MAX(0, term->cursor.col + off)); + } } static bool terminal_check_focus(TerminalState *const s) @@ -1129,6 +1161,13 @@ static int terminal_execute(VimState *state, int key) s->got_bsl = true; break; } + if (s->term->suspended) { + s->term->opts.resume_cb(s->term->opts.data); + // XXX: detecting continued process via waitpid() on SIGCHLD doesn't always work + // (e.g. on macOS), so also consider it continued after sending SIGCONT. + terminal_set_state(s->term, false); + break; + } if (s->term->closed) { s->close = true; return 0; @@ -1393,15 +1432,23 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, int *te } Buffer terminal_buf(const Terminal *term) + FUNC_ATTR_NONNULL_ALL { return term->buf_handle; } bool terminal_running(const Terminal *term) + FUNC_ATTR_NONNULL_ALL { return !term->closed; } +bool terminal_suspended(const Terminal *term) + FUNC_ATTR_NONNULL_ALL +{ + return term->suspended; +} + void terminal_notify_theme(Terminal *term, bool dark) FUNC_ATTR_NONNULL_ALL { @@ -2063,7 +2110,8 @@ static bool send_mouse_event(Terminal *term, int c) } int offset; - if (term->forward_mouse && mouse_win->w_buffer->terminal == term && row >= 0 + if (!term->suspended && !term->closed + && term->forward_mouse && mouse_win->w_buffer->terminal == term && row >= 0 && (grid > 1 || row + mouse_win->w_winbar_height < mouse_win->w_height) && col >= (offset = win_col_off(mouse_win)) && (grid > 1 || col < mouse_win->w_width)) { diff --git a/src/nvim/terminal.h b/src/nvim/terminal.h index 528796f4c1..816cc4a1f2 100644 --- a/src/nvim/terminal.h +++ b/src/nvim/terminal.h @@ -9,6 +9,7 @@ typedef void (*terminal_write_cb)(const char *buffer, size_t size, void *data); typedef void (*terminal_resize_cb)(uint16_t width, uint16_t height, void *data); +typedef void (*terminal_resume_cb)(void *data); typedef void (*terminal_close_cb)(void *data); typedef struct { @@ -16,6 +17,7 @@ typedef struct { uint16_t width, height; terminal_write_cb write_cb; terminal_resize_cb resize_cb; + terminal_resume_cb resume_cb; terminal_close_cb close_cb; bool force_crlf; } TerminalOptions; diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 6eb43e10ba..c41cb665ec 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -450,6 +450,105 @@ describe(':terminal buffer', function() ]]) assert_alive() end) + + describe('handles suspended PTY process', function() + if skip(is_os('win'), 'N/A for Windows') then + return + end + + --- @param external_resume boolean + local function test_term_process_suspend_resume(external_resume) + command('set mousemodel=extend') + local pid = eval('jobpid(&channel)') + vim.uv.kill(pid, 'sigstop') + screen:expect([[ + tty ready | + |*4 + ^[Process suspended] | + {5:-- TERMINAL --} | + ]]) + command('set laststatus=0 | botright vsplit') + screen:expect([[ + tty ready │tty ready | + │ |*4 + [Process suspended] │^[Process suspended] | + {5:-- TERMINAL --} | + ]]) + -- Resize is detected by the process on resume. + if external_resume then + vim.uv.kill(pid, 'sigcont') + else + feed('a') + end + screen:expect([[ + tty ready │tty ready | + rows: 6, cols: 25 │rows: 6, cols: 25 | + │^ | + │ |*3 + {5:-- TERMINAL --} | + ]]) + tt.enable_mouse() + tt.feed_data('mouse enabled\n\n\n\n') + screen:expect([[ + rows: 6, cols: 25 │rows: 6, cols: 25 | + mouse enabled │mouse enabled | + │ |*3 + │^ | + {5:-- TERMINAL --} | + ]]) + api.nvim_input_mouse('right', 'press', '', 0, 0, 25) + screen:expect({ any = vim.pesc('"!!^') }) + api.nvim_input_mouse('right', 'release', '', 0, 0, 25) + screen:expect({ any = vim.pesc('#!!^') }) + vim.uv.kill(pid, 'sigstop') + local s1 = [[ + rows: 6, cols: 25 │rows: 6, cols: 25 | + mouse enabled │mouse enabled | + │ |*3 + [Process suspended] │^[Process suspended] | + {5:-- TERMINAL --} | + ]] + screen:expect(s1) + -- Mouse isn't forwarded when process is suspended. + api.nvim_input_mouse('right', 'press', '', 0, 1, 27) + api.nvim_input_mouse('right', 'release', '', 0, 1, 27) + screen:expect([[ + rows: 6, cols: 25 │rows: 6, cols: 25 | + mo{108:use enabled} │mo^u{108:se enabled} | + {108: } │{108: } |*3 + [Process suspended] │[Process suspended] | + {5:-- VISUAL --} | + ]]) + feed('i') + screen:expect(s1) + if external_resume then + vim.uv.kill(pid, 'sigcont') + else + feed('a') + end + screen:expect([[ + rows: 6, cols: 25 │rows: 6, cols: 25 | + mouse enabled │mouse enabled | + │ |*3 + #!! │ #!!^ | + {5:-- TERMINAL --} | + ]]) + -- Mouse is forwarded after process is resumed. + api.nvim_input_mouse('right', 'press', '', 0, 0, 28) + screen:expect({ any = vim.pesc('"$!^') }) + api.nvim_input_mouse('right', 'release', '', 0, 0, 28) + screen:expect({ any = vim.pesc('#$!^') }) + end + + it('resumed by an external signal', function() + skip(is_os('mac'), 'FIXME: does not work on macOS') + test_term_process_suspend_resume(true) + end) + + it('resumed by pressing a key', function() + test_term_process_suspend_resume(false) + end) + end) end) describe(':terminal buffer', function() diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 2a2100694f..c1c94de2b3 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -100,11 +100,11 @@ describe('TUI', function() ]]) else -- resuming works on other platforms screen:expect([[ - ^ | |*5 + ^[Process suspended] | {5:-- TERMINAL --} | ]]) - exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + n.feed('') screen:expect(s0) end feed_data(':') @@ -4409,7 +4409,6 @@ describe('TUI client', function() it('suspend/resume works with multiple clients', function() t.skip(is_os('win'), 'N/A for Windows') local server_super, screen_server, screen_client = start_tui_and_remote_client() - local server_super_exec_lua = tt.make_lua_executor(server_super) local screen_normal = [[ Hello, Worl^d | @@ -4419,8 +4418,8 @@ describe('TUI client', function() {5:-- TERMINAL --} | ]] local screen_suspended = [[ - ^ | |*5 + ^[Process suspended] | {5:-- TERMINAL --} | ]] @@ -4433,12 +4432,12 @@ describe('TUI client', function() screen_server:expect({ grid = screen_suspended }) -- Resume the remote client. - exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + n.feed('') screen_client:expect({ grid = screen_normal }) screen_server:expect({ grid = screen_suspended, unchanged = true }) -- Resume the embedding client. - server_super_exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + server_super:request('nvim_input', '') screen_server:expect({ grid = screen_normal }) screen_client:expect({ grid = screen_normal, unchanged = true }) @@ -4448,7 +4447,7 @@ describe('TUI client', function() screen_server:expect({ grid = screen_suspended }) -- Resume the remote client. - exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + n.feed('') screen_client:expect({ grid = screen_normal }) screen_server:expect({ grid = screen_suspended, unchanged = true }) @@ -4458,12 +4457,12 @@ describe('TUI client', function() screen_server:expect({ grid = screen_suspended, unchanged = true }) -- Resume the embedding client. - server_super_exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + server_super:request('nvim_input', '') screen_server:expect({ grid = screen_normal }) screen_client:expect({ grid = screen_suspended, unchanged = true }) -- Resume the remote client. - exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigcont')]]) + n.feed('') screen_client:expect({ grid = screen_normal }) screen_server:expect({ grid = screen_normal, unchanged = true })