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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Hirohito HigashiandClaude Opus 5 authored and Christian Brabandt committed 2026-08-23 18:47:52 +00:00
1 parent 4025cec645
commit 6308df41bf
5 files changed
+37 -17

No files matched your search

+7
View File
@@ -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)
<
+3 -2
View File
@@ -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 ~
+12 -13
View File
@@ -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"))
+13 -2
View File
@@ -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:')
+2
View File
@@ -763,6 +763,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
997,
/**/
996,
/**/