patch 9.2.0153: No support to act as a channel server

Commit: 
https://github.com/vim/vim/commit/ba861f8c533023edb3cde6ff7c91926f0a248e54
Author: Yasuhiro Matsumoto <[email protected]>
Date:   Fri Mar 13 19:05:45 2026 +0000

    patch 9.2.0153: No support to act as a channel server
    
    Problem:  Vim can only act as a channel client (ch_open). There is
              no way for an external process to initiate a connection
              to a running Vim instance using the Channel API.
    Solution: Implement ch_listen() and the underlying server-side
              socket logic. This allows Vim to listen on a port or
              Unix domain socket. When a client connects, a new
              channel is automatically created and passed to a
              user-defined callback (Yasuhiro Matsumoto).
    
    closes: #19231
    
    Co-authored-by: Christian Brabandt <[email protected]>
    Co-authored-by: Copilot <[email protected]>
    Signed-off-by: Yasuhiro Matsumoto <[email protected]>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index d95f459a9..45072ab16 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -1,4 +1,4 @@
-*builtin.txt*  For Vim version 9.2.  Last change: 2026 Feb 14
+*builtin.txt*  For Vim version 9.2.  Last change: 2026 Mar 13
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -106,6 +106,8 @@ ch_evalraw({handle}, {string} [, {options}])
 ch_getbufnr({handle}, {what})  Number  get buffer number for {handle}/{what}
 ch_getjob({channel})           Job     get the Job of {channel}
 ch_info({handle})              String  info about channel {handle}
+ch_listen({address} [, {options}])
+                               Channel listen on {address}
 ch_log({msg} [, {handle}])     none    write {msg} in the channel log file
 ch_logfile({fname} [, {mode}]) none    start logging channel activity
 ch_open({address} [, {options}])
diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt
index 36694af9f..7ad10fa0c 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 Feb 25
+*channel.txt*  For Vim version 9.2.  Last change: 2026 Mar 13
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -114,6 +114,32 @@ Vim to write lines in log file: >
        call ch_logfile('channellog', 'w')
 See |ch_logfile()|.
 
+You can also make Vim act as a server using |ch_listen()|.  This does not
+require an external server program.
+
+                                                       *channel-listen-demo*
+Start Vim and create a listening channel: >
+       func OnAccept(channel, clientaddr)
+         " Log the connection
+         echomsg "Accepted connection from " .. a:clientaddr
+
+         " Get current time and send it to the client
+         let current_time = strftime("%Y-%m-%d %H:%M:%S")
+         call ch_sendraw(a:channel, "Vim Server Time: " .. current_time .. "
")
+
+         " Optional: close immediately if you only want to provide the time
+         call ch_close(a:channel)
+       endfunc
+
+       " Start listening on port 8765
+       let server = ch_listen('localhost:8765', {"callback": "OnAccept"})
+
+From another Vim instance (or any program) you can connect to it: >
+       let channel = ch_open('localhost:8765')
+
+When done, close the server channel: >
+       call ch_close(server)
+
 ==============================================================================
 3. Opening a channel                                   *channel-open*
 
@@ -641,6 +667,33 @@ ch_info({handle})                                          
*ch_info()*
 <
                Return type: dict<any>
 
+ch_listen({address} [, {options}])             *E1573* *E1574* *ch_listen()*
+               Listen on {address} for incoming channel connections.
+               This creates a server-side channel, unlike |ch_open()|
+               which connects to an existing server.
+               Returns a Channel.  Use |ch_status()| to check for failure.
+
+               {address} is a String, see |channel-address| for the possible
+               accepted forms.  Note: IPv6 is not yet supported.
+
+               If {options} is given it must be a |Dictionary|.
+               See |channel-open-options|.
+               The "callback" in {options} is invoked when a new
+               connection is accepted.  It receives two arguments: the
+               new Channel and the client address as a String (e.g.
+               "127.0.0.1:12345").
+
+               Use |ch_open()| to connect to an existing server instead.
+
+               See |channel-listen-demo| for an example.
+
+               Can also be used as a |method|: >
+                       GetAddress()->ch_listen()
+<
+               {only available when compiled with the |+channel| feature}
+
+               Return type: channel
+
 ch_log({msg} [, {handle}])                                     *ch_log()*
                Write String {msg} in the channel log file, if it was opened
                with |ch_logfile()|.
@@ -695,6 +748,9 @@ ch_open({address} [, {options}])                            
*ch_open()*
                If {options} is given it must be a |Dictionary|.
                See |channel-open-options|.
 
+               Use |ch_listen()| to listen for incoming connections
+               instead.
+
                Can also be used as a |method|: >
                        GetAddress()->ch_open()
 <
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 723880e1b..f5950e085 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -4770,6 +4770,8 @@ E157      sign.txt        /*E157*
 E1570  builtin.txt     /*E1570*
 E1571  builtin.txt     /*E1571*
 E1572  options.txt     /*E1572*
+E1573  channel.txt     /*E1573*
+E1574  channel.txt     /*E1574*
 E158   sign.txt        /*E158*
 E159   sign.txt        /*E159*
 E16    cmdline.txt     /*E16*
@@ -6584,6 +6586,7 @@ ch_evalraw()      channel.txt     /*ch_evalraw()*
 ch_getbufnr()  channel.txt     /*ch_getbufnr()*
 ch_getjob()    channel.txt     /*ch_getjob()*
 ch_info()      channel.txt     /*ch_info()*
+ch_listen()    channel.txt     /*ch_listen()*
 ch_log()       channel.txt     /*ch_log()*
 ch_logfile()   channel.txt     /*ch_logfile()*
 ch_open()      channel.txt     /*ch_open()*
@@ -6634,6 +6637,7 @@ channel-demo      channel.txt     /*channel-demo*
 channel-drop   channel.txt     /*channel-drop*
 channel-functions      usr_41.txt      /*channel-functions*
 channel-functions-details      channel.txt     /*channel-functions-details*
+channel-listen-demo    channel.txt     /*channel-listen-demo*
 channel-mode   channel.txt     /*channel-mode*
 channel-more   channel.txt     /*channel-more*
 channel-noblock        channel.txt     /*channel-noblock*
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index b5c572a07..e6ca80457 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1287,23 +1287,24 @@ Testing:                                    
*test-functions*
 
 Inter-process communication:               *channel-functions*
        ch_canread()            check if there is something to read
-       ch_open()               open a channel
        ch_close()              close a channel
        ch_close_in()           close the in part of a channel
+       ch_evalexpr()           evaluate an expression over channel
+       ch_evalraw()            evaluate a raw string over channel
+       ch_getbufnr()           get the buffer number for a channel
+       ch_getjob()             get the Job of a channel
+       ch_info()               get information about a channel
+       ch_listen()             listen on a channel
+       ch_log()                write a message in the channel log
+       ch_logfile()            start logging channel activity
+       ch_open()               open a channel
        ch_read()               read a message from a channel
        ch_readblob()           read a Blob from a channel
        ch_readraw()            read a raw message from a channel
        ch_sendexpr()           send a JSON message over a channel
        ch_sendraw()            send a raw message over a channel
-       ch_evalexpr()           evaluate an expression over channel
-       ch_evalraw()            evaluate a raw string over channel
+       ch_setoptions()         set channel options
        ch_status()             get status of a channel
-       ch_getbufnr()           get the buffer number of a channel
-       ch_getjob()             get the job associated with a channel
-       ch_info()               get channel information
-       ch_log()                write a message in the channel log file
-       ch_logfile()            set the channel log file
-       ch_setoptions()         set the options for a channel
        json_encode()           encode an expression to a JSON string
        json_decode()           decode a JSON string to Vim types
        js_encode()             encode an expression to a JSON string
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 1e7dcf70c..cee00a282 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2.  Last change: 2026 Mar 12
+*version9.txt* For Vim version 9.2.  Last change: 2026 Mar 13
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -52606,6 +52606,10 @@ Added ~
 -----
 Various syntax, indent and other plugins were added.
 
+Functions: ~
+
+|ch_listen()|          listen on {address}
+
 Autocommands: ~
 
 |SessionLoadPre|       before loading a |Session| file
diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim
index dc0356eae..ed7ce5fb0 100644
--- a/runtime/syntax/vim.vim
+++ b/runtime/syntax/vim.vim
@@ -2,7 +2,7 @@
 " Language:       Vim script
 " Maintainer:     Hirohito Higashi <h.east.727 ATMARK gmail.com>
 "         Doug Kearns <[email protected]>
-" Last Change:    2026 Mar 11
+" Last Change:    2026 Mar 13
 " Former Maintainer: Charles E. Campbell
 
 " DO NOT CHANGE DIRECTLY.
@@ -153,8 +153,8 @@ syn case match
 
 " Function Names {{{2
 " GEN_SYN_VIM: vimFuncName, START_STR='syn keyword vimFuncName contained', 
END_STR=''
-syn keyword vimFuncName contained abs acos add and append appendbufline argc 
argidx arglistid argv asin assert_beeps assert_equal assert_equalfile 
assert_exception assert_fails assert_false assert_inrange assert_match 
assert_nobeep assert_notequal assert_notmatch assert_report assert_true atan 
atan2 autocmd_add autocmd_delete autocmd_get balloon_gettext balloon_show 
balloon_split base64_decode base64_encode bindtextdomain blob2list blob2str 
browse browsedir bufadd bufexists buflisted bufload bufloaded bufname bufnr 
bufwinid bufwinnr byte2line byteidx byteidxcomp call ceil ch_canread ch_close 
ch_close_in ch_evalexpr ch_evalraw ch_getbufnr ch_getjob ch_info ch_log 
ch_logfile ch_open ch_read ch_readblob ch_readraw ch_sendexpr ch_sendraw 
ch_setoptions ch_status changenr
-syn keyword vimFuncName contained char2nr charclass charcol charidx chdir 
cindent clearmatches cmdcomplete_info col complete complete_add complete_check 
complete_info confirm copy cos cosh count cscope_connection cursor debugbreak 
deepcopy delete deletebufline did_filetype diff diff_filler diff_hlID 
digraph_get digraph_getlist digraph_set digraph_setlist echoraw empty environ 
err_teapot escape eval eventhandler executable execute exepath exists 
exists_compiled exp expand expandcmd extend extendnew feedkeys filecopy 
filereadable filewritable filter finddir findfile flatten flattennew float2nr 
floor fmod fnameescape fnamemodify foldclosed foldclosedend foldlevel foldtext 
foldtextresult foreach foreground fullcommand funcref function garbagecollect 
get getbufinfo
+syn keyword vimFuncName contained abs acos add and append appendbufline argc 
argidx arglistid argv asin assert_beeps assert_equal assert_equalfile 
assert_exception assert_fails assert_false assert_inrange assert_match 
assert_nobeep assert_notequal assert_notmatch assert_report assert_true atan 
atan2 autocmd_add autocmd_delete autocmd_get balloon_gettext balloon_show 
balloon_split base64_decode base64_encode bindtextdomain blob2list blob2str 
browse browsedir bufadd bufexists buflisted bufload bufloaded bufname bufnr 
bufwinid bufwinnr byte2line byteidx byteidxcomp call ceil ch_canread ch_close 
ch_close_in ch_evalexpr ch_evalraw ch_getbufnr ch_getjob ch_info ch_listen 
ch_log ch_logfile ch_open ch_read ch_readblob ch_readraw ch_sendexpr ch_sendraw 
ch_setoptions ch_status
+syn keyword vimFuncName contained changenr char2nr charclass charcol charidx 
chdir cindent clearmatches cmdcomplete_info col complete complete_add 
complete_check complete_info confirm copy cos cosh count cscope_connection 
cursor debugbreak deepcopy delete deletebufline did_filetype diff diff_filler 
diff_hlID digraph_get digraph_getlist digraph_set digraph_setlist echoraw empty 
environ err_teapot escape eval eventhandler executable execute exepath exists 
exists_compiled exp expand expandcmd extend extendnew feedkeys filecopy 
filereadable filewritable filter finddir findfile flatten flattennew float2nr 
floor fmod fnameescape fnamemodify foldclosed foldclosedend foldlevel foldtext 
foldtextresult foreach foreground fullcommand funcref function garbagecollect 
get getbufinfo
 syn keyword vimFuncName contained getbufline getbufoneline getbufvar 
getcellpixels getcellwidths getchangelist getchar getcharmod getcharpos 
getcharsearch getcharstr getcmdcomplpat getcmdcompltype getcmdline getcmdpos 
getcmdprompt getcmdscreenpos getcmdtype getcmdwintype getcompletion 
getcompletiontype getcurpos getcursorcharpos getcwd getenv getfontname getfperm 
getfsize getftime getftype getimstatus getjumplist getline getloclist 
getmarklist getmatches getmousepos getmouseshape getpid getpos getqflist getreg 
getreginfo getregion getregionpos getregtype getscriptinfo getstacktrace 
gettabinfo gettabvar gettabwinvar gettagstack gettext getwininfo getwinpos 
getwinposx getwinposy getwinvar glob glob2regpat globpath has has_key 
haslocaldir hasmapto histadd histdel
 syn keyword vimFuncName contained histget histnr hlID hlexists hlget hlset 
hostname iconv id indent index indexof input inputdialog inputlist inputrestore 
inputsave inputsecret insert instanceof interrupt invert isabsolutepath 
isdirectory isinf islocked isnan items job_getchannel job_info job_setoptions 
job_start job_status job_stop join js_decode js_encode json_decode json_encode 
keys keytrans len libcall libcallnr line line2byte lispindent list2blob 
list2str list2tuple listener_add listener_flush listener_remove localtime log 
log10 luaeval map maparg mapcheck maplist mapnew mapset match matchadd 
matchaddpos matcharg matchbufline matchdelete matchend matchfuzzy matchfuzzypos 
matchlist matchstr matchstrlist matchstrpos max menu_info min mkdir mode mzeval 
nextnonblank
 syn keyword vimFuncName contained ngettext nr2char or pathshorten perleval 
popup_atcursor popup_beval popup_clear popup_close popup_create popup_dialog 
popup_filter_menu popup_filter_yesno popup_findecho popup_findinfo 
popup_findpreview popup_getoptions popup_getpos popup_hide popup_list 
popup_locate popup_menu popup_move popup_notification popup_setbuf 
popup_setoptions popup_settext popup_show pow preinserted prevnonblank printf 
prompt_getprompt prompt_setcallback prompt_setinterrupt prompt_setprompt 
prop_add prop_add_list prop_clear prop_find prop_list prop_remove prop_type_add 
prop_type_change prop_type_delete prop_type_get prop_type_list pum_getpos 
pumvisible py3eval pyeval pyxeval rand range readblob readdir readdirex 
readfile redraw_listener_add redraw_listener_remove
diff --git a/src/channel.c b/src/channel.c
index 92b95f9b6..78a462cc9 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -1394,7 +1394,307 @@ theend:
     return channel;
 }
 
-    void
+/*
+ * Implements ch_listen().
+ */
+    channel_T *
+channel_listen_func(typval_T *argvars)
+{
+    char_u     *address;
+    char_u     *p;
+    char       *rest;
+    int                port;
+    int                is_unix = FALSE;
+    jobopt_T    opt;
+    channel_T  *channel = NULL;
+
+    if (in_vim9script()
+           && (check_for_string_arg(argvars, 0) == FAIL
+               || check_for_opt_dict_arg(argvars, 1) == FAIL))
+       return NULL;
+
+    address = tv_get_string(&argvars[0]);
+    if (argvars[1].v_type != VAR_UNKNOWN
+           && check_for_nonnull_dict_arg(argvars, 1) == FAIL)
+       return NULL;
+
+    if (*address == NUL)
+    {
+       semsg(_(e_invalid_argument_str), address);
+       return NULL;
+    }
+
+    if (!STRNCMP(address, "unix:", 5))
+    {
+       is_unix = TRUE;
+       address += 5;
+       port = 0;
+    }
+    else if (*address == '[')
+    {
+       // ipv6 address
+       p = vim_strchr(address + 1, ']');
+       if (p == NULL || *++p != ':')
+       {
+           semsg(_(e_invalid_argument_str), address);
+           return NULL;
+       }
+       port = strtol((char *)(p + 1), &rest, 10);
+       if (port <= 0 || port >= 65536 || *rest != NUL)
+       {
+           semsg(_(e_invalid_argument_str), address);
+           return NULL;
+       }
+       // strip '[' and ']'
+       ++address;
+       *(p - 1) = NUL;
+    }
+    else
+    {
+       // ipv4 address
+       p = vim_strchr(address, ':');
+       if (p == NULL)
+       {
+           semsg(_(e_invalid_argument_str), address);
+           return NULL;
+       }
+       port = strtol((char *)(p + 1), &rest, 10);
+       if (port <= 0 || port >= 65536 || *rest != NUL)
+       {
+           semsg(_(e_invalid_argument_str), address);
+           return NULL;
+       }
+       *p = NUL;
+    }
+
+    // parse options
+    clear_job_options(&opt);
+    opt.jo_mode = CH_MODE_JSON;
+    opt.jo_timeout = 2000;
+    if (get_job_options(&argvars[1], &opt,
+           JO_MODE_ALL + JO_CB_ALL + JO_TIMEOUT_ALL, 0) == FAIL)
+       goto theend;
+    if (opt.jo_timeout < 0)
+    {
+       emsg(_(e_invalid_argument));
+       goto theend;
+    }
+
+    if (is_unix)
+       channel = channel_listen_unix((char *)address, NULL);
+    else
+       channel = channel_listen((char *)address, port, NULL);
+    if (channel != NULL)
+    {
+       opt.jo_set = JO_ALL;
+       channel_set_options(channel, &opt);
+    }
+theend:
+    free_job_options(&opt);
+    return channel;
+}
+
+/*
+ * Listen to a socket for connections.
+ * Returns the channel for success.
+ * Returns NULL for failure.
+ */
+    channel_T *
+channel_listen(
+       char *hostname,
+       int port_in,
+       void (*nb_close_cb)(void))
+{
+    int                        sd = -1;
+    struct sockaddr_in server;
+    struct hostent     *host;
+    int                        val = 1;
+    channel_T          *channel;
+
+    channel = add_channel();
+    if (channel == NULL)
+    {
+       ch_error(NULL, "Cannot allocate channel.");
+       return NULL;
+    }
+
+    // Get the server internet address and put into addr structure
+    // fill in the socket address structure and bind to port
+    vim_memset((char *)&server, 0, sizeof(server));
+    server.sin_family = AF_INET;
+    server.sin_port = htons(port_in);
+    if (hostname != NULL && *hostname != NUL)
+    {
+       if ((host = gethostbyname(hostname)) == NULL)
+       {
+           ch_error(channel, "in gethostbyname() in channel_listen()");
+           PERROR(_(e_gethostbyname_in_channel_listen));
+           channel_free(channel);
+           return NULL;
+       }
+       {
+           char                *p;
+
+           // When using host->h_addr_list[0] directly ubsan warns for it to
+           // not be aligned.  First copy the pointer to avoid that.
+           memcpy(&p, &host->h_addr_list[0], sizeof(p));
+           memcpy((char *)&server.sin_addr, p, host->h_length);
+       }
+    }
+    else
+       server.sin_addr.s_addr = htonl(INADDR_ANY);
+
+    sd = socket(AF_INET, SOCK_STREAM, 0);
+    if (sd == -1)
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in socket() in channel_listen().");
+       PERROR(_(e_cannot_listen_on_port));
+       channel_free(channel);
+       return NULL;
+    }
+
+#ifdef MSWIN
+    if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,
+                                   (const char *)&val, sizeof(val)) < 0)
+#else
+    if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,
+                                   &val, sizeof(val)) < 0)
+#endif
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in setsockopt() in channel_listen().");
+       PERROR(_(e_cannot_listen_on_port));
+       sock_close(sd);
+       channel_free(channel);
+       return NULL;
+    }
+
+    // Bind the socket to the port
+    if (bind(sd, (struct sockaddr *)&server, sizeof(server)) < 0)
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in bind() in channel_listen().");
+       PERROR(_(e_cannot_listen_on_port));
+       sock_close(sd);
+       channel_free(channel);
+       return NULL;
+    }
+
+    if (listen(sd, 5) < 0)
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in listen() in channel_listen().");
+       PERROR(_(e_cannot_listen_on_port));
+       sock_close(sd);
+       channel_free(channel);
+       return NULL;
+    }
+
+    channel->ch_listen = TRUE;
+    channel->CH_SOCK_FD = (sock_T)sd;
+    channel->ch_nb_close_cb = nb_close_cb;
+    channel->ch_hostname = (char *)vim_strsave((char_u *)hostname);
+    channel->ch_port = port_in;
+    channel->ch_to_be_closed |= (1U << PART_SOCK);
+
+#ifdef FEAT_GUI
+    channel_gui_register_one(channel, PART_SOCK);
+#endif
+
+    return channel;
+}
+
+/*
+ * Listen to a Unix domain socket channel.
+ * Returns the channel for success.
+ * Returns NULL for failure.
+ */
+    channel_T *
+channel_listen_unix(
+       char *path,
+       void (*nb_close_cb)(void))
+{
+    int                        sd = -1;
+    struct sockaddr_un server;
+    channel_T          *channel;
+    size_t             path_len;
+    size_t             server_len;
+
+    if (path == NULL || *path == NUL)
+    {
+       semsg(_(e_invalid_argument_str), path == NULL ? (char *)"" : path);
+       return NULL;
+    }
+
+    path_len = STRLEN(path);
+    if (path_len >= sizeof(server.sun_path))
+    {
+       semsg(_(e_invalid_argument_str), path);
+       return NULL;
+    }
+
+    channel = add_channel();
+    if (channel == NULL)
+    {
+       ch_error(NULL, "Cannot allocate channel.");
+       return NULL;
+    }
+
+    CLEAR_FIELD(server);
+    server.sun_family = AF_UNIX;
+    STRNCPY(server.sun_path, path, sizeof(server.sun_path) - 1);
+
+    sd = socket(AF_UNIX, SOCK_STREAM, 0);
+    if (sd == -1)
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in socket() in channel_listen_unix().");
+       PERROR(_(e_cannot_listen_on_port));
+       channel_free(channel);
+       return NULL;
+    }
+
+    // Unlink the socket in case it already exists
+    unlink(server.sun_path);
+
+    // Bind the socket to the path
+    server_len = offsetof(struct sockaddr_un, sun_path) + path_len + 1;
+    if (bind(sd, (struct sockaddr *)&server, (int)server_len) < 0)
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in bind() in channel_listen_unix().");
+       PERROR(_(e_cannot_listen_on_port));
+       sock_close(sd);
+       channel_free(channel);
+       return NULL;
+    }
+
+    if (listen(sd, 5) < 0)
+    {
+       SOCK_ERRNO;
+       ch_error(channel, "in listen() in channel_listen_unix().");
+       PERROR(_(e_cannot_listen_on_port));
+       sock_close(sd);
+       channel_free(channel);
+       return NULL;
+    }
+
+    channel->ch_listen = TRUE;
+    channel->CH_SOCK_FD = (sock_T)sd;
+    channel->ch_nb_close_cb = nb_close_cb;
+    channel->ch_hostname = (char *)vim_strsave((char_u *)path);
+    channel->ch_port = 0;
+    channel->ch_to_be_closed |= (1U << PART_SOCK);
+
+#ifdef FEAT_GUI
+    channel_gui_register_one(channel, PART_SOCK);
+#endif
+
+    return channel;
+}
+
+     void
 ch_close_part(channel_T *channel, ch_part_T part)
 {
     sock_T *fd = &channel->ch_part[part].ch_fd;
@@ -3861,6 +4161,65 @@ channel_read(channel_T *channel, ch_part_T part, char 
*func)
     {
        if (channel_wait(channel, fd, 0) != CW_READY)
            break;
+       if (channel->ch_listen)
+       {
+           sock_T              newfd;
+           socklen_t           socklen;
+           channel_T           *newchannel;
+           typval_T            argv[2];
+           char_u              namebuf[256];
+           struct sockaddr_storage     client;
+
+           newchannel = add_channel();
+           if (newchannel == NULL)
+           {
+               ch_error(NULL, "Cannot allocate channel.");
+               return;
+           }
+           socklen = sizeof(client);
+           newfd = accept(fd, (struct sockaddr*)&client, &socklen);
+           if (newfd < 0)
+           {
+               ch_error(NULL, "Cannot accept channel.");
+               channel_free(newchannel);
+               return;
+           }
+           newchannel->CH_SOCK_FD = (sock_T)newfd;
+           newchannel->ch_to_be_closed |= (1U << PART_SOCK);
+
+           if (client.ss_family == AF_INET)
+               vim_snprintf((char *)namebuf, sizeof(namebuf), "%s:%d",
+                   inet_ntoa(((struct sockaddr_in*)&client)->sin_addr),
+                   ntohs(((struct sockaddr_in*)&client)->sin_port));
+#ifdef HAVE_INET_NTOP
+           else if (client.ss_family == AF_INET6)
+           {
+               char addr[INET6_ADDRSTRLEN];
+
+               inet_ntop(AF_INET6,
+                       &((struct sockaddr_in6*)&client)->sin6_addr,
+                       addr, sizeof(addr));
+               vim_snprintf((char *)namebuf, sizeof(namebuf), "[%s]:%d",
+                       addr,
+                       ntohs(((struct sockaddr_in6*)&client)->sin6_port));
+           }
+#endif
+           else if (client.ss_family == AF_UNIX)
+               vim_snprintf((char *)namebuf, sizeof(namebuf),
+                                                           "unix:anonymous");
+           else
+               vim_snprintf((char *)namebuf, sizeof(namebuf), "unknown");
+           ++safe_to_invoke_callback;
+           ++newchannel->ch_refcount;
+           argv[0].v_type = VAR_CHANNEL;
+           argv[0].vval.v_channel = newchannel;
+           argv[1].v_type = VAR_STRING;
+           argv[1].vval.v_string = vim_strsave(namebuf);
+           invoke_callback(newchannel, &channel->ch_callback, argv);
+           --safe_to_invoke_callback;
+           clear_tv(&argv[1]);
+           return;
+       }
        if (use_socket)
            len = sock_read(fd, (char *)buf, MAXMSGSIZE);
        else
@@ -5298,6 +5657,18 @@ f_ch_info(typval_T *argvars, typval_T *rettv UNUSED)
        channel_info(channel, rettv->vval.v_dict);
 }
 
+/*
+ * "ch_listen()" function
+ */
+    void
+f_ch_listen(typval_T *argvars, typval_T *rettv)
+{
+    rettv->v_type = VAR_CHANNEL;
+    if (check_restricted() || check_secure())
+       return;
+    rettv->vval.v_channel = channel_listen_func(argvars);
+}
+
 /*
  * "ch_open()" function
  */
diff --git a/src/errors.h b/src/errors.h
index b909c0fbf..9c4e485ef 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3807,3 +3807,9 @@ EXTERN char e_no_redraw_listener_callbacks_defined[]
 #endif
 EXTERN char e_leadtab_requires_tab[]
        INIT(= N_("E1572: 'listchars' field \"leadtab\" requires \"tab\" to be 
specified"));
+#ifdef FEAT_JOB_CHANNEL
+EXTERN char e_cannot_listen_on_port[]
+       INIT(= N_("E1573: Cannot listen on port"));
+EXTERN char e_gethostbyname_in_channel_listen[]
+       INIT(= N_("E1574: gethostbyname(): cannot resolve hostname in 
channel_listen()"));
+#endif
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 3c872af56..9b7008415 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2083,6 +2083,8 @@ static const funcentry_T global_functions[] =
                        ret_job,            JOB_FUNC(f_ch_getjob)},
     {"ch_info",                1, 1, FEARG_1,      arg1_chan_or_job,
                        ret_dict_any,       JOB_FUNC(f_ch_info)},
+    {"ch_listen",      1, 2, FEARG_1,      arg2_string_dict,
+                       ret_channel,        JOB_FUNC(f_ch_listen)},
     {"ch_log",         1, 2, FEARG_1,      arg2_string_chan_or_job,
                        ret_void,           f_ch_log},
     {"ch_logfile",     1, 2, FEARG_1,      arg2_string,
diff --git a/src/po/vim.pot b/src/po/vim.pot
index c9d9a0aa8..446a0b91f 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim
"
 "Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-03-11 20:02+0000
"
+"POT-Creation-Date: 2026-03-13 18:26+0000
"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
 "Language-Team: LANGUAGE <[email protected]>
"
@@ -8848,6 +8848,12 @@ msgstr ""
 msgid "E1572: 'listchars' field \"leadtab\" requires \"tab\" to be specified"
 msgstr ""
 
+msgid "E1573: Cannot listen on port"
+msgstr ""
+
+msgid "E1574: gethostbyname(): cannot resolve hostname in channel_listen()"
+msgstr ""
+
 #. type of cmdline window or 0
 #. result of cmdline window or 0
 #. buffer of cmdline window or NULL
diff --git a/src/proto/channel.pro b/src/proto/channel.pro
index 794e60b22..d0c2cd042 100644
--- a/src/proto/channel.pro
+++ b/src/proto/channel.pro
@@ -8,6 +8,9 @@ int free_unused_channels_contents(int copyID, int mask);
 void free_unused_channels(int copyID, int mask);
 void channel_gui_register_all(void);
 channel_T *channel_open(const char *hostname, int port, int waittime, void 
(*nb_close_cb)(void));
+channel_T *channel_listen_func(typval_T *argvars);
+channel_T *channel_listen(char *hostname, int port_in, void 
(*nb_close_cb)(void));
+channel_T *channel_listen_unix(char *path, void (*nb_close_cb)(void));
 void ch_close_part(channel_T *channel, ch_part_T part);
 void channel_set_pipes(channel_T *channel, sock_T in, sock_T out, sock_T err);
 void channel_set_job(channel_T *channel, job_T *job, jobopt_T *options);
@@ -44,6 +47,7 @@ void f_ch_close_in(typval_T *argvars, typval_T *rettv);
 void f_ch_getbufnr(typval_T *argvars, typval_T *rettv);
 void f_ch_getjob(typval_T *argvars, typval_T *rettv);
 void f_ch_info(typval_T *argvars, typval_T *rettv);
+void f_ch_listen(typval_T *argvars, typval_T *rettv);
 void f_ch_open(typval_T *argvars, typval_T *rettv);
 void f_ch_read(typval_T *argvars, typval_T *rettv);
 void f_ch_readblob(typval_T *argvars, typval_T *rettv);
diff --git a/src/structs.h b/src/structs.h
index 5f4e1fc16..2b8cb3db1 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2708,10 +2708,11 @@ struct channel_S {
     int                ch_to_be_freed; // When TRUE channel must be freed when 
it's
                                // safe to invoke callbacks.
     int                ch_error;       // When TRUE an error was reported.  
Avoids
-                               // giving pages full of error messages when
-                               // the other side has exited, only mention the
-                               // first error until the connection works
-                               // again.
+    // giving pages full of error messages when
+    // the other side has exited, only mention the
+    // first error until the connection works
+    // again.
+    int                ch_listen;      // When TRUE channel is listen socket.
 
     void       (*ch_nb_close_cb)(void);
                                // callback for Netbeans when channel is
diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim
index 8ee72c9f3..431eaab26 100644
--- a/src/testdir/test_channel.vim
+++ b/src/testdir/test_channel.vim
@@ -556,7 +556,7 @@ func Test_raw_pipe()
   " ch_canread().
   " Also test the non-blocking option.
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
+        \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
   call assert_equal(v:t_job, type(job))
   call assert_equal("run", job_status(job))
 
@@ -639,7 +639,7 @@ func Test_raw_pipe_blob()
   " ch_canread().
   " Also test the non-blocking option.
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
+        \ {'mode': 'raw', 'drop': 'never', 'noblock': 1})
   call assert_equal(v:t_job, type(job))
   call assert_equal("run", job_status(job))
 
@@ -713,7 +713,7 @@ endfunc
 func Test_nl_read_file()
   call writefile(['echo something', 'echoerr wrong', 'double this'], 'Xinput', 
'D')
   let g:job = job_start(s:python . " test_channel_pipe.py",
-       \ {'in_io': 'file', 'in_name': 'Xinput'})
+        \ {'in_io': 'file', 'in_name': 'Xinput'})
   call assert_equal("run", job_status(g:job))
   try
     let handle = job_getchannel(g:job)
@@ -729,7 +729,7 @@ endfunc
 
 func Test_nl_write_out_file()
   let g:job = job_start(s:python . " test_channel_pipe.py",
-       \ {'out_io': 'file', 'out_name': 'Xoutput'})
+        \ {'out_io': 'file', 'out_name': 'Xoutput'})
   call assert_equal("run", job_status(g:job))
   try
     let handle = job_getchannel(g:job)
@@ -746,7 +746,7 @@ endfunc
 
 func Test_nl_write_err_file()
   let g:job = job_start(s:python . " test_channel_pipe.py",
-       \ {'err_io': 'file', 'err_name': 'Xoutput'})
+        \ {'err_io': 'file', 'err_name': 'Xoutput'})
   call assert_equal("run", job_status(g:job))
   try
     let handle = job_getchannel(g:job)
@@ -762,7 +762,7 @@ endfunc
 
 func Test_nl_write_both_file()
   let g:job = job_start(s:python . " test_channel_pipe.py",
-       \ {'out_io': 'file', 'out_name': 'Xoutput', 'err_io': 'out'})
+        \ {'out_io': 'file', 'out_name': 'Xoutput', 'err_io': 'out'})
   call assert_equal("run", job_status(g:job))
   try
     let handle = job_getchannel(g:job)
@@ -929,7 +929,7 @@ endfunc
 
 func Test_pipe_both_to_buffer()
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'out_io': 'buffer', 'out_name': 'pipe-err', 'err_io': 'out'})
+        \ {'out_io': 'buffer', 'out_name': 'pipe-err', 'err_io': 'out'})
   call assert_equal("run", job_status(job))
   let handle = job_getchannel(job)
   call assert_equal(bufnr('pipe-err'), ch_getbufnr(handle, 'out'))
@@ -1037,7 +1037,7 @@ endfunc
 
 func Test_pipe_to_nameless_buffer()
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'out_io': 'buffer'})
+        \ {'out_io': 'buffer'})
   call assert_equal("run", job_status(job))
   try
     let handle = job_getchannel(job)
@@ -1056,7 +1056,7 @@ func Test_pipe_to_buffer_json()
   CheckFunction reltimefloat
 
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'out_io': 'buffer', 'out_mode': 'json'})
+        \ {'out_io': 'buffer', 'out_mode': 'json'})
   call assert_equal("run", job_status(job))
   try
     let handle = job_getchannel(job)
@@ -1089,9 +1089,9 @@ func Test_pipe_io_two_buffers()
   set buftype=nofile
 
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'in_io': 'buffer', 'in_name': 'pipe-input', 'in_top': 0,
-       \  'out_io': 'buffer', 'out_name': 'pipe-output',
-       \  'block_write': 1})
+        \ {'in_io': 'buffer', 'in_name': 'pipe-input', 'in_top': 0,
+        \  'out_io': 'buffer', 'out_name': 'pipe-output',
+        \  'block_write': 1})
   call assert_equal("run", job_status(job))
   try
     exe "normal Gaecho hello\<CR>"
@@ -1120,9 +1120,9 @@ func Test_pipe_io_one_buffer()
   set buftype=nofile
 
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'in_io': 'buffer', 'in_name': 'pipe-io', 'in_top': 0,
-       \  'out_io': 'buffer', 'out_name': 'pipe-io',
-       \  'block_write': 1})
+        \ {'in_io': 'buffer', 'in_name': 'pipe-io', 'in_top': 0,
+        \  'out_io': 'buffer', 'out_name': 'pipe-io',
+        \  'block_write': 1})
   call assert_equal("run", job_status(job))
   try
     exe "normal Goecho hello\<CR>"
@@ -1151,9 +1151,9 @@ func Test_write_to_buffer_and_scroll()
       wincmd w
       call deletebufline('Xscrollbuffer', 1, '$')
       if has('win32')
-       let cmd = ['cmd', '/c', 'echo sometext']
+        let cmd = ['cmd', '/c', 'echo sometext']
       else
-       let cmd = [&shell, &shellcmdflag, 'echo sometext']
+        let cmd = [&shell, &shellcmdflag, 'echo sometext']
       endif
       call job_start(cmd, #{out_io: 'buffer', out_name: 'Xscrollbuffer'})
   END
@@ -1170,7 +1170,7 @@ func Test_pipe_null()
   " We cannot check that no I/O works, we only check that the job starts
   " properly.
   let job = job_start(s:python . " test_channel_pipe.py something",
-       \ {'in_io': 'null'})
+        \ {'in_io': 'null'})
   call assert_equal("run", job_status(job))
   try
     call assert_equal('something', ch_read(job))
@@ -1179,7 +1179,7 @@ func Test_pipe_null()
   endtry
 
   let job = job_start(s:python . " test_channel_pipe.py err-out",
-       \ {'out_io': 'null'})
+        \ {'out_io': 'null'})
   call assert_equal("run", job_status(job))
   try
     call assert_equal('err-out', ch_read(job, {"part": "err"}))
@@ -1188,7 +1188,7 @@ func Test_pipe_null()
   endtry
 
   let job = job_start(s:python . " test_channel_pipe.py something",
-       \ {'err_io': 'null'})
+        \ {'err_io': 'null'})
   call assert_equal("run", job_status(job))
   try
     call assert_equal('something', ch_read(job))
@@ -1271,10 +1271,10 @@ func Test_out_cb()
     let g:Ch_errmsg = self.thisis . a:msg
   endfunc
   let job = job_start(s:python . " test_channel_pipe.py",
-       \ {'out_cb': dict.outHandler,
-       \  'out_mode': 'json',
-       \  'err_cb': dict.errHandler,
-       \  'err_mode': 'json'})
+        \ {'out_cb': dict.outHandler,
+        \  'out_mode': 'json',
+        \  'err_cb': dict.errHandler,
+        \  'err_mode': 'json'})
   call assert_equal("run", job_status(job))
   call test_garbagecollect_now()
   try
@@ -1320,8 +1320,8 @@ func Test_out_close_cb()
     let s:counter += 1
   endfunc
   let job = job_start(s:python . " test_channel_pipe.py quit now",
-       \ {'out_cb': 'OutHandler',
-       \  'close_cb': 'CloseHandler'})
+        \ {'out_cb': 'OutHandler',
+        \  'close_cb': 'CloseHandler'})
   " the job may be done quickly, also accept "dead"
   call assert_match('^\%(dead\|run\)$', job_status(job))
   try
@@ -1340,7 +1340,7 @@ func Test_read_in_close_cb()
     let g:Ch_received = ch_read(a:chan)
   endfunc
   let job = job_start(s:python . " test_channel_pipe.py quit now",
-       \ {'close_cb': 'CloseHandler'})
+        \ {'close_cb': 'CloseHandler'})
   " the job may be done quickly, also accept "dead"
   call assert_match('^\%(dead\|run\)$', job_status(job))
   try
@@ -1360,7 +1360,7 @@ func Test_read_in_close_cb_incomplete()
     endwhile
   endfunc
   let job = job_start(s:python . " test_channel_pipe.py incomplete",
-       \ {'close_cb': 'CloseHandler'})
+        \ {'close_cb': 'CloseHandler'})
   " the job may be done quickly, also accept "dead"
   call assert_match('^\%(dead\|run\)$', job_status(job))
   try
@@ -1427,7 +1427,7 @@ func Test_exit_cb_wipes_buf()
   let g:wipe_buf = bufnr('')
 
   let job = job_start(has('win32') ? 'cmd /D /c echo:' : ['true'],
-       \ {'exit_cb': 'ExitCbWipe'})
+        \ {'exit_cb': 'ExitCbWipe'})
   let timer = timer_start(300, {-> feedkeys("\<Esc>", 'nt')}, {'repeat': 5})
   call feedkeys(repeat('g', 1000) . 'o', 'ntx!')
   call WaitForAssert({-> assert_equal("dead", job_status(job))})
@@ -2265,16 +2265,16 @@ func Test_zz_nl_err_to_out_pipe()
     let found_stop = 0
     for l in loglines
       if l =~ 'Test_zz_nl_err_to_out_pipe'
-       let found_test = 1
+        let found_test = 1
       endif
       if l =~ 'SEND on.*echo something'
-       let found_send = 1
+        let found_send = 1
       endif
       if l =~ 'RECV on.*something'
-       let found_recv = 1
+        let found_recv = 1
       endif
       if l =~ 'Stopping job with'
-       let found_stop = 1
+        let found_stop = 1
       endif
     endfor
     call assert_equal(1, found_test)
@@ -2758,6 +2758,62 @@ func LspTests(port)
   " call ch_logfile('', 'w')
 endfunc
 
+let g:server_received_addr = ''
+let g:server_received_msg = ''
+
+func s:test_listen_accept(ch, addr)
+    let g:server_received_addr = a:addr
+    let g:server_received_msg = ch_readraw(a:ch)
+endfunction
+
+func Test_listen()
+    call ch_log('Test_listen()')
+    let server = ch_listen('127.0.0.1:12345', {'callback': 
function('s:test_listen_accept')})
+    if ch_status(server) == 'fail'
+        call assert_report("Can't listen channel")
+        return
+    endif
+    let handle = ch_open('127.0.0.1:12345', s:chopt)
+    if ch_status(handle) == 'fail'
+        call assert_report("Can't open channel")
+        return
+    endif
+    call ch_sendraw(handle, 'hello')
+    call WaitFor('"" != g:server_received_msg')
+    call ch_close(handle)
+    call ch_close(server)
+    call assert_equal('hello', g:server_received_msg)
+    call assert_match('127.0.0.1:', g:server_received_addr)
+endfunc
+
+func Test_listen_invalid_address()
+    call ch_log('Test_listen_invalid_address()')
+
+    " empty address
+    call assert_fails("call ch_listen('')", 'E475:')
+
+    " missing port
+    call assert_fails("call ch_listen('localhost')", 'E475:')
+
+    " port number too large
+    call assert_fails("call ch_listen('localhost:99999')", 'E475:')
+
+    " port number zero
+    call assert_fails("call ch_listen('localhost:0')", 'E475:')
+
+    " port number negative
+    call assert_fails("call ch_listen('localhost:-1')", 'E475:')
+
+    " invalid ipv6 format (missing closing bracket)
+    call assert_fails("call ch_listen('[::1:8765')", 'E475:')
+
+    " invalid ipv6 format (missing port)
+    call assert_fails("call ch_listen('[::1]')", 'E475:')
+
+    " TODO: IPv6 should actually work
+    call assert_fails("call ch_listen('[::1]:9999')", 'E1574:')
+endfunc
+
 func Test_channel_lsp_mode()
   " The channel lsp mode test is flaky and gives the same error.
   let g:giveup_same_error = 0
diff --git a/src/version.c b/src/version.c
index 78491aa86..184a7d327 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    153,
 /**/
     152,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1w18D0-003pSQ-MF%40256bit.org.

Raspunde prin e-mail lui