From: Hirohito Higashi Date: Tue, 28 Apr 2026 21:03:12 +0000 (+0000) Subject: patch 9.2.0412: channel: term_start() out_cb/err_cb no longer deliver raw chunks X-Git-Tag: v9.2.0412^0 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=41c3379bdf944dcee7f64b8e95094c03e2dce968;p=thirdparty%2Fvim.git patch 9.2.0412: channel: term_start() out_cb/err_cb no longer deliver raw chunks Problem: channel: term_start() out_cb/err_cb no longer deliver raw chunks (regression from patch 9.2.0224, breaks callers like vim-fugitive that parse multi-line output) (D. Ben Knoble, after v9.2.0224) Solution: Remove the PTY-specific per-line splitting in may_invoke_callback() so RAW callbacks again receive the raw chunk as returned by read(), preserving embedded NL. If per-line handling is desired, the callback must split "msg" on NL and strip the trailing CR itself; document this behavior in term_start(). Replace Test_term_start_cb_per_line() with Test_term_start_cb_raw_chunk() to verify the raw-chunk contract. fixes: #20041 closes: #20045 Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: Yasuhiro Matsumoto Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index ce0ceb45be..26cdc6a006 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -1,4 +1,4 @@ -*channel.txt* For Vim version 9.2. Last change: 2026 Apr 15 +*channel.txt* For Vim version 9.2. Last change: 2026 Apr 28 VIM REFERENCE MANUAL by Bram Moolenaar diff --git a/runtime/doc/terminal.txt b/runtime/doc/terminal.txt index 6c9430dc81..01fd88ca66 100644 --- a/runtime/doc/terminal.txt +++ b/runtime/doc/terminal.txt @@ -965,6 +965,20 @@ term_start({cmd} [, {options}]) *term_start()* input and one output handle, with no separate handle for stderr. + Note: term_start() always uses RAW mode for its callbacks. + "out_cb" and "err_cb" receive the raw chunk of data as read + from the OS. A single callback invocation may contain + multiple lines separated by NL, and (for stdout via a pty) + each line may have a trailing CR from the line discipline + (ONLCR). If per-line handling is desired, the callback must + split "msg" on NL and strip the trailing CR itself. + Example: > + func Handle(ch, msg) + for line in split(a:msg, "\n") + echom substitute(line, '\r$', '', '') + endfor + endfunc +< There are extra options: "term_name" name to use for the buffer name, instead of the command name. diff --git a/src/channel.c b/src/channel.c index dc8645b060..a0b7b953e2 100644 --- a/src/channel.c +++ b/src/channel.c @@ -3510,46 +3510,7 @@ may_invoke_callback(channel_T *channel, ch_part_T part) // invoke the channel callback ch_log(channel, "Invoking channel callback %s", (char *)callback->cb_name); -#ifdef FEAT_TERMINAL - // For a terminal job in RAW mode (term_start()), split msg on - // NL and invoke the callback once per line with trailing CR - // stripped. This ensures out_cb/err_cb receive one line at a - // time regardless of how much data arrives in a single read. - if (ch_mode == CH_MODE_RAW && msg != NULL - && channel->ch_job != NULL - && channel->ch_job->jv_tty_out != NULL) - { - char_u *cp = msg; - char_u *nl; - - while ((nl = vim_strchr(cp, NL)) != NULL) - { - long_u len = (long_u)(nl - cp); - - if (len > 0 && cp[len - 1] == CAR) - --len; - argv[1].vval.v_string = vim_strnsave(cp, len); - if (argv[1].vval.v_string != NULL) - invoke_callback(channel, callback, argv); - vim_free(argv[1].vval.v_string); - cp = nl + 1; - } - if (*cp != NUL) - { - long_u len = STRLEN(cp); - - if (len > 0 && cp[len - 1] == CAR) - --len; - argv[1].vval.v_string = vim_strnsave(cp, len); - if (argv[1].vval.v_string != NULL) - invoke_callback(channel, callback, argv); - vim_free(argv[1].vval.v_string); - } - argv[1].vval.v_string = msg; - } - else -#endif - invoke_callback(channel, callback, argv); + invoke_callback(channel, callback, argv); } } } diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim index ceb5532637..abdaed0dc1 100644 --- a/src/testdir/test_channel.vim +++ b/src/testdir/test_channel.vim @@ -2933,13 +2933,15 @@ func Test_error_callback_terminal() unlet! g:out g:error endfunc -" Verify that term_start() with out_cb/err_cb delivers one line per callback -" call (no embedded newlines, no trailing CR), matching the user's expectation. -func Test_term_start_cb_per_line() +" Verify that term_start() with out_cb/err_cb delivers data in RAW mode, +" preserving embedded newlines in the raw chunk received from read(). If +" per-line handling is desired, it is the callback's responsibility to split +" on NL and strip the trailing CR. +func Test_term_start_cb_raw_chunk() CheckUnix CheckFeature terminal let g:Ch_msgs = [] - let script_file = 'Xterm_cb_per_line.sh' + let script_file = 'Xterm_cb_raw_chunk.sh' call writefile(["#!/bin/sh", \ "printf 'err:1\\nerr:2\\n' >&2", \ "printf 'out:3\\n'"], script_file, 'D') @@ -2947,10 +2949,16 @@ func Test_term_start_cb_per_line() let ptybuf = term_start('./' .. script_file, { \ 'out_cb': {ch, msg -> add(g:Ch_msgs, msg)}, \ 'err_cb': {ch, msg -> add(g:Ch_msgs, msg)}}) - call WaitForAssert({-> assert_equal(3, len(g:Ch_msgs))}, 5000) - " Each line must arrive as a separate callback call with no embedded CR/NL. - call assert_equal(['err:1', 'err:2', 'out:3'], g:Ch_msgs) + " Wait until both the raw stderr chunk and a stdout chunk have arrived. + call WaitForAssert({-> assert_true( + \ index(g:Ch_msgs, "err:1\nerr:2\n") >= 0 + \ && match(g:Ch_msgs, 'out:3') >= 0)}, 5000) + " stderr (via pipe) arrives as a single raw chunk with embedded NL, + " not split per line. stdout (via PTY) is delivered, but its exact + " CR/LF shape depends on the PTY line discipline, so we only check that + " 'out:3' appears somewhere in the received chunks. call job_stop(term_getjob(ptybuf)) + exe 'bwipe! ' .. ptybuf unlet g:Ch_msgs endfunc diff --git a/src/version.c b/src/version.c index d3ed299226..778a227b7a 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 412, /**/ 411, /**/