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) <noreply@anthropic.com>
Co-Authored-By: Yasuhiro Matsumoto <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Hirohito Higashi
2026-04-28 21:03:12 +00:00
committed by Christian Brabandt
parent e7745b7cbf
commit 41c3379bdf
5 changed files with 33 additions and 48 deletions
+1 -1
View File
@@ -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
+14
View File
@@ -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.
+1 -40
View File
@@ -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);
}
}
}
+15 -7
View File
@@ -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
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
412,
/**/
411,
/**/