patch 9.2.0412: channel: term_start() out_cb/err_cb no longer deliver raw chunks
Commit:
https://github.com/vim/vim/commit/41c3379bdf944dcee7f64b8e95094c03e2dce968
Author: Hirohito Higashi <
h.eas...@gmail.com>
Date: Tue Apr 28 21:03:12 2026 +0000
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) <
nor...@anthropic.com>
Co-Authored-By: Yasuhiro Matsumoto <
matt...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt
index ce0ceb45b..26cdc6a00 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 6c9430dc8..01fd88ca6 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, "
")
+ echom substitute(line, '
$', '', '')
+ 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 dc8645b06..a0b7b953e 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 ceb553263..abdaed0dc 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
err:2
") >= 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 d3ed29922..778a227b7 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,
/**/