From 63642ebf80cb1142e84fa6ded98f7541f15739be Mon Sep 17 00:00:00 2001 From: Ayaan <138162656+def3r@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:24:41 +0530 Subject: [PATCH] refactor(terminal): impl "[Process exited]" in Lua #38343 Problem: "[Process exited]" is implemented in C with anonymous namespace and users have no way to hide it. Solution: - Handle "TermClose" event in Lua. - User can delete the "nvim.terminal" augroup to avoid "[Process exited]". --- runtime/doc/vim_diff.txt | 1 + runtime/lua/vim/_core/defaults.lua | 51 +++++++++++++++++++ src/nvim/terminal.c | 43 ++++------------ test/functional/terminal/ex_terminal_spec.lua | 4 +- 4 files changed, 65 insertions(+), 34 deletions(-) diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index f3c9e9158f..f88971abc4 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -180,6 +180,7 @@ nvim.terminal: - BufReadCmd: Treats "term://" buffers as |terminal| buffers. |terminal-start| - TermClose: A |terminal| buffer started with no arguments (which thus uses 'shell') and which exits with no error is closed automatically. +- TermClose: Displays the "[Process exited]" virtual text. - TermRequest: The terminal emulator responds to OSC background and foreground requests, indicating (1) a black background and white foreground when Nvim option 'background' is "dark" or (2) a white background and black foreground diff --git a/runtime/lua/vim/_core/defaults.lua b/runtime/lua/vim/_core/defaults.lua index 5ef11ee94a..51f26d845b 100644 --- a/runtime/lua/vim/_core/defaults.lua +++ b/runtime/lua/vim/_core/defaults.lua @@ -578,6 +578,54 @@ do end, }) + local nvim_terminal_exitmsg_ns = vim.api.nvim_create_namespace('nvim.terminal.exitmsg') + + --- @param buf integer + --- @param msg string + --- @param pos integer + local function set_terminal_exitmsg(buf, msg, pos) + vim.api.nvim_buf_set_extmark(buf, nvim_terminal_exitmsg_ns, pos, 0, { + virt_text = { { msg, nil } }, + virt_text_pos = 'overlay', + }) + end + + vim.api.nvim_create_autocmd('TermClose', { + group = nvim_terminal_augroup, + nested = true, + desc = 'Displays the "[Process exited]" virtual text', + callback = function(ev) + if not vim.api.nvim_buf_is_valid(ev.buf) then + return + end + + local buf = vim.bo[ev.buf] + local pos = ev.data.pos ---@type integer + local buf_has_exitmsg = #( + vim.api.nvim_buf_get_extmarks(ev.buf, nvim_terminal_exitmsg_ns, 0, -1, {}) + ) > 0 + + -- `nvim_open_term` buffers do not have any attached chan + local msg = buf.channel == 0 and '[Terminal closed]' + or ('[Process exited %d]'):format(vim.v.event.status) + + -- TermClose may be queued before TermOpen if the process + -- exits before `terminal_open` is called. Don't display + -- the msg now, let TermOpen display it. + if buf.buftype ~= 'terminal' or buf_has_exitmsg then + vim.api.nvim_create_autocmd('TermOpen', { + buffer = ev.buf, + once = true, + callback = function() + set_terminal_exitmsg(ev.buf, msg, pos) + end, + }) + return + end + set_terminal_exitmsg(ev.buf, msg, pos) + end, + }) + vim.api.nvim_create_autocmd('TermRequest', { group = nvim_terminal_augroup, desc = 'Handles OSC foreground/background color requests', @@ -694,6 +742,9 @@ do vim.keymap.set({ 'n', 'x', 'o' }, ']]', function() jump_to_prompt(nvim_terminal_prompt_ns, 0, ev.buf, vim.v.count1) end, { buffer = ev.buf, desc = 'Jump [count] shell prompts forward' }) + + -- If the terminal buffer is being reused, clear the previous exit msg + vim.api.nvim_buf_clear_namespace(ev.buf, nvim_terminal_exitmsg_ns, 0, -1) end, }) diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 8b79395eed..2df6bdfac5 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -64,7 +64,6 @@ #include "nvim/event/multiqueue.h" #include "nvim/event/time.h" #include "nvim/ex_docmd.h" -#include "nvim/extmark.h" #include "nvim/getchar.h" #include "nvim/globals.h" #include "nvim/grid.h" @@ -211,7 +210,6 @@ struct terminal { VTermTerminator termrequest_terminator; ///< Terminator (BEL or ST) used in the termrequest size_t refcount; // reference count - uint32_t exitmsg_id; }; static VTermScreenCallbacks vterm_screen_callbacks = { @@ -681,10 +679,6 @@ void terminal_close(Terminal **termpp, int status) // If called from buf_close_terminal() after the process has already exited, we // only need to call the close callback to clean up the terminal object. only_destroy = true; - // Buffer may be reused so delete the "[Process exited]" msg - if (buf) { - extmark_del_id(buf, (uint32_t)-1, term->exitmsg_id); - } } else { // flush any pending changes to the buffer if (!exiting) { @@ -695,6 +689,7 @@ void terminal_close(Terminal **termpp, int status) term->closed = true; } + int pos = buf ? buf->b_ml.ml_line_count - 1 : 0; if (status == -1 || exiting) { // If this was called by buf_close_terminal() (status is -1), or if exiting, we // must inform the buffer the terminal no longer exists so that buf_freeall() @@ -714,37 +709,15 @@ void terminal_close(Terminal **termpp, int status) } else if (!only_destroy) { // Associated channel has been closed and the editor is not exiting. // Do not call the close callback now. Wait for the user to press a key. - char msg[sizeof("[Process exited ]") + NUMBUFLEN]; - if (((Channel *)term->opts.data)->streamtype == kChannelStreamInternal) { - snprintf(msg, sizeof msg, "[Terminal closed]"); - } else { - snprintf(msg, sizeof msg, "[Process exited %d]", status); - } - - // Show the msg as virtual text instead of adding it to buffer - VirtTextChunk *chunk = xmalloc(sizeof(VirtTextChunk)); - *chunk = (VirtTextChunk) { .text = xstrdup(msg), .hl_id = -1 }; - DecorVirtText *virt_text = xmalloc(sizeof(DecorVirtText)); - *virt_text = (DecorVirtText) { - .priority = DECOR_PRIORITY_BASE, - .pos = kVPosWinCol, - .data.virt_text = { .items = chunk, .size = 1 } - }; - DecorInline decor = { - .ext = true, .data.ext = { .sh_idx = DECOR_ID_INVALID, .vt = virt_text } - }; - - int pos = MIN(row_to_linenr(term, term->cursor.row), - buf->b_ml.ml_line_count - 1); - extmark_set(buf, (uint32_t)-1, &term->exitmsg_id, pos, 0, -1, 0, - decor, 0, true, false, true, false, NULL); - // Redraw statusline to show the exit code. FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { if (wp->w_buffer == buf) { wp->w_redr_status = true; } } + + // Gets the line number to display "[Process exited]" virt text + pos = MIN(row_to_linenr(term, term->cursor.row), pos); } if (only_destroy) { @@ -756,7 +729,13 @@ void terminal_close(Terminal **termpp, int status) dict_T *dict = get_v_event(&save_v_event); tv_dict_add_nr(dict, S_LEN("status"), status); tv_dict_set_keys_readonly(dict); - apply_autocmds(EVENT_TERMCLOSE, NULL, NULL, false, buf); + + MAXSIZE_TEMP_DICT(data, 1); + PUT_C(data, "pos", INTEGER_OBJ(pos)); + + apply_autocmds_group(EVENT_TERMCLOSE, NULL, NULL, status >= 0, AUGROUP_ALL, + buf, NULL, &DICT_OBJ(data)); + restore_v_event(dict, &save_v_event); } } diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua index 0e38645bf5..f08576ca91 100644 --- a/test/functional/terminal/ex_terminal_spec.lua +++ b/test/functional/terminal/ex_terminal_spec.lua @@ -215,7 +215,7 @@ local function test_terminal_with_fake_shell(backslash) command('terminal') screen:expect([[ ^ready $ | - [Process exited 0] | + | |*2 ]]) end) @@ -297,7 +297,7 @@ local function test_terminal_with_fake_shell(backslash) command('terminal') screen:expect([[ ^ready $ | - [Process exited 0] | + | |*2 ]]) eq('term://', string.match(eval('bufname("%")'), '^term://'))