Commit: patch 9.2.0997: ch_sendexpr() cannot answer a request named with a string id

5 views
Skip to first unread message

Christian Brabandt

unread,
Aug 23, 2026, 3:00:16 PM (4 days ago) Aug 23
to vim...@googlegroups.com
patch 9.2.0997: ch_sendexpr() cannot answer a request named with a string id

Commit: https://github.com/vim/vim/commit/6308df41bfb29c22586b202b72c7e2b2052a09d0
Author: Hirohito Higashi <h.eas...@gmail.com>
Date: Sun Aug 23 18:47:52 2026 +0000

patch 9.2.0997: ch_sendexpr() cannot answer a request named with a string id

Problem: A request that a server named with a string cannot be
answered, since ch_sendexpr() rejects an "id" that is not a
Number.
Solution: Require a Number only when Vim has to match a reply, which
is with ch_evalexpr() or when a callback is given. Send the
message as it is otherwise (Hirohito Higashi).

fixes: #14091
fixes: #19003
closes: #21114
closes: #21126

Co-Authored-By: Claude Opus 5 (1M context) <nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt
index 839dcc296..ee0fe673b 100644
--- a/runtime/doc/channel.txt
+++ b/runtime/doc/channel.txt
@@ -812,6 +812,13 @@ ch_sendexpr({handle}, {expr} [, {options}]) *ch_sendexpr()*
If a response message is not expected for {expr}, then don't
specify the "callback" item in {options}.

+ In "lsp" mode an "id" item in {expr} is sent as it is when no
+ response is expected, which is how a request from the server
+ is answered; it may be of any type the server used. When a
+ response is expected, with |ch_evalexpr()| or with the
+ "callback" item, Vim assigns the ID itself and the item must
+ be a |Number|.
+
Can also be used as a |method|: >
GetChannel()->ch_sendexpr(expr)
<
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index c0f746f2c..768f9e12a 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 Aug 20
+*version9.txt* For Vim version 9.2. Last change: 2026 Aug 23


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52705,7 +52705,8 @@ Changed ~
- |extend()| and |extendnew()| also accept |Blobs|.
- events like |CursorMoved| are no longer triggered for what happened while
they were ignored with 'eventignore'.
-
+- |ch_sendexpr()| accepts a non-Number "id" in "lsp" mode when no response is
+ expected, so a request from the server can be answered.

*added-9.3*
Added ~
diff --git a/src/channel.c b/src/channel.c
index 0519a157b..3fc9120b3 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -5120,6 +5120,7 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
{
dict_T *d;
dictitem_T *di;
+ char *key = ch_mode == CH_MODE_LSP ? "id" : "seq";

// return an empty dict by default
if (rettv_dict_alloc(rettv) == FAIL)
@@ -5129,22 +5130,20 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
return;

d = argvars[1].vval.v_dict;
- if (ch_mode == CH_MODE_LSP)
- di = dict_find(d, (char_u *)"id", -1);
- else
- di = dict_find(d, (char_u *)"seq", -1);
- if (di != NULL && di->di_tv.v_type != VAR_NUMBER)
+ di = dict_find(d, (char_u *)key, -1);
+ if (argvars[2].v_type == VAR_DICT
+ && dict_has_key(argvars[2].vval.v_dict, "callback"))
+ callback_present = TRUE;
+
+ // The id is what a reply is matched by, so it must be a number when
+ // one is waited for.
+ if (di != NULL && di->di_tv.v_type != VAR_NUMBER
+ && (ch_mode == CH_MODE_DAP || eval || callback_present))
{
- // only number type is supported for the 'id' or 'seq' item
- semsg(_(e_invalid_value_for_argument_str),
- ch_mode == CH_MODE_LSP ? "id" : "seq");
+ semsg(_(e_invalid_value_for_argument_str), key);
return;
}

- if (argvars[2].v_type == VAR_DICT)
- if (dict_has_key(argvars[2].vval.v_dict, "callback"))
- callback_present = TRUE;
-
if (ch_mode == CH_MODE_DAP)
{
// DAP message always has a sequence number (id)
@@ -5169,7 +5168,7 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
// When sending an expression, if the message has an 'id' item,
// then use it.
id = 0;
- if (di != NULL)
+ if (di != NULL && di->di_tv.v_type == VAR_NUMBER)
id = di->di_tv.vval.v_number;
}
if (ch_mode == CH_MODE_LSP && !dict_has_key(d, "jsonrpc"))
diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim
index f35a1b855..6aab5b166 100644
--- a/src/testdir/test_channel.vim
+++ b/src/testdir/test_channel.vim
@@ -2809,9 +2809,20 @@ func LspTests(port)
" \ 'result': {'method': 'echo', 'jsonrpc': '2.0',
" \ 'params': {'m': 'raw-message'}}}], g:lspNotif)

+ " Test for sending a message with a String id, which is what answering a
+ " request from a server that named it with one takes
+ let g:lspNotif = []
+ call ch_sendexpr(ch, #{method: 'echo', id: 'e8a1-4c2f',
+ \ params: #{s: 'msg-with-string-id'}})
+ " Send a ping to wait for all the notification messages to arrive
+ call assert_equal('alive', ch_evalexpr(ch, #{method: 'ping'}).result)
+ call assert_equal([#{jsonrpc: '2.0', result:
+ \ #{method: 'echo', jsonrpc: '2.0', id: 'e8a1-4c2f',
+ \ params: #{s: 'msg-with-string-id'}}}], g:lspNotif)
+
" Invalid arguments to ch_evalexpr() and ch_sendexpr()
- call assert_fails('call ch_sendexpr(ch, #{method: "cookie", id: "cookie"})',
- \ 'E475:')
+ call assert_fails('call ch_sendexpr(ch, #{method: "cookie", id: "cookie"},'
+ \ .. ' #{callback: "LspOtCb"})', 'E475:')
call assert_fails('call ch_evalexpr(ch, #{method: "ping", id: [{}]})', 'E475:')
call assert_fails('call ch_evalexpr(ch, [1, 2, 3])', 'E1206:')
call assert_fails('call ch_sendexpr(ch, "abc")', 'E1206:')
diff --git a/src/version.c b/src/version.c
index 37e823d3d..3c26d9443 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 997,
/**/
996,
/**/
Reply all
Reply to author
Forward
0 new messages