]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0412: channel: term_start() out_cb/err_cb no longer deliver raw chunks v9.2.0412
authorHirohito Higashi <h.east.727@gmail.com>
Tue, 28 Apr 2026 21:03:12 +0000 (21:03 +0000)
committerChristian Brabandt <cb@256bit.org>
Tue, 28 Apr 2026 21:06:45 +0000 (21:06 +0000)
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>
runtime/doc/channel.txt
runtime/doc/terminal.txt
src/channel.c
src/testdir/test_channel.vim
src/version.c

index ce0ceb45be21a6354a39c80b76209d04cf0721fa..26cdc6a0067dffdf6bcaa1c7278d97ff27395586 100644 (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
index 6c9430dc81ff9c8e15229f9cefc344c50454b658..01fd88ca660e300929ee2a84498cb8de23b4b705 100644 (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.
index dc8645b060d9480b12ea8f72b4bb1ff41abcded6..a0b7b953e2d68a371bd2de181da7364ba7a65504 100644 (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);
            }
        }
     }
index ceb553263770a8f3e47c349f38a8039547e8a776..abdaed0dc1773560626ec725668c5ea93e20d9b8 100644 (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
 
index d3ed299226bea7f318ddfa51943cd5448b998fc7..778a227b7a2b4752ecbce9f49a24818f519bc5fd 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    412,
 /**/
     411,
 /**/