mirror of
https://github.com/vim/vim.git
synced 2026-05-28 00:21:37 +02:00
patch 9.2.0060: No support for the DAP channel mode
Problem: No support for the DAP channel mode
Solution: Add native channel support for the debug-adapter-protocol
(Foxe Chen)
closes: #19432
Signed-off-by: Foxe Chen <chen.foxe@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
committed by
Christian Brabandt
parent
e8432bc5d5
commit
b7eb0c2d38
+49
-15
@@ -1,4 +1,4 @@
|
||||
*channel.txt* For Vim version 9.2. Last change: 2026 Feb 18
|
||||
*channel.txt* For Vim version 9.2. Last change: 2026 Feb 25
|
||||
|
||||
|
||||
VIM REFERENCE MANUAL by Bram Moolenaar
|
||||
@@ -26,6 +26,7 @@ The Netbeans interface also uses a channel. |netbeans|
|
||||
13. Controlling a job |job-control|
|
||||
14. Using a prompt buffer |prompt-buffer|
|
||||
15. Language Server Protocol |language-server-protocol|
|
||||
16. Debug Adapter Protocol |debug-adapter-protocol|
|
||||
|
||||
*E1277*
|
||||
{only when compiled with the |+channel| feature for channel stuff}
|
||||
@@ -56,6 +57,7 @@ NL every message ends in a NL (newline) character
|
||||
JSON JSON encoding |json_encode()|
|
||||
JS JavaScript style JSON-like encoding |js_encode()|
|
||||
LSP Language Server Protocol encoding |language-server-protocol|
|
||||
DAP Debug Adapter Protocol encoding |debug-adapter-protocol|
|
||||
|
||||
Common combination are:
|
||||
- Using a job connected through pipes in NL mode. E.g., to run a style
|
||||
@@ -143,6 +145,7 @@ unreachable on the network.
|
||||
"nl" - Use messages that end in a NL character
|
||||
"raw" - Use raw messages
|
||||
"lsp" - Use language server protocol encoding
|
||||
"dap" - Use debug adapter protocol encoding
|
||||
*channel-callback* *E921*
|
||||
"callback" A function that is called when a message is received that is
|
||||
not handled otherwise (e.g. a JSON message with ID zero). It
|
||||
@@ -153,8 +156,9 @@ unreachable on the network.
|
||||
endfunc
|
||||
let channel = ch_open("localhost:8765", {"callback": "Handle"})
|
||||
<
|
||||
When "mode" is "json" or "js" or "lsp" the "msg" argument is
|
||||
the body of the received message, converted to Vim types.
|
||||
When "mode" is any of "json", "js", "lsp" or "dap" the "msg"
|
||||
argument is the body of the received message, converted to Vim
|
||||
types.
|
||||
When "mode" is "nl" the "msg" argument is one message,
|
||||
excluding the NL.
|
||||
When "mode" is "raw" the "msg" argument is the whole message
|
||||
@@ -537,7 +541,8 @@ ch_evalexpr({handle}, {expr} [, {options}]) *ch_evalexpr()*
|
||||
according to the type of channel. The function cannot be used
|
||||
with a raw channel. See |channel-use|.
|
||||
{handle} can be a Channel or a Job that has a Channel.
|
||||
When using the "lsp" channel mode, {expr} must be a |Dict|.
|
||||
When using the "lsp" or "dap" channel mode, {expr} must be a
|
||||
|Dict|.
|
||||
*E917*
|
||||
{options} must be a Dictionary. It must not have a "callback"
|
||||
entry. It can have a "timeout" entry to specify the timeout
|
||||
@@ -545,8 +550,8 @@ ch_evalexpr({handle}, {expr} [, {options}]) *ch_evalexpr()*
|
||||
|
||||
ch_evalexpr() waits for a response and returns the decoded
|
||||
expression. When there is an error or timeout it returns an
|
||||
empty |String| or, when using the "lsp" channel mode, returns an
|
||||
empty |Dict|.
|
||||
empty |String| or, when using the "lsp" or "dap" channel mode,
|
||||
returns an empty |Dict|.
|
||||
|
||||
Note that while waiting for the response, Vim handles other
|
||||
messages. You need to make sure this doesn't cause trouble.
|
||||
@@ -627,7 +632,7 @@ ch_info({handle}) *ch_info()*
|
||||
"err_io" "out", "null", "pipe", "file" or "buffer"
|
||||
"err_timeout" timeout in msec
|
||||
"in_status" "open" or "closed"
|
||||
"in_mode" "NL", "RAW", "JSON", "JS" or "LSP"
|
||||
"in_mode" "NL", "RAW", "JSON", "JS" or "LSP" or "DAP"
|
||||
"in_io" "null", "pipe", "file" or "buffer"
|
||||
"in_timeout" timeout in msec
|
||||
|
||||
@@ -733,14 +738,15 @@ ch_sendexpr({handle}, {expr} [, {options}]) *ch_sendexpr()*
|
||||
with a raw channel.
|
||||
See |channel-use|. *E912*
|
||||
{handle} can be a Channel or a Job that has a Channel.
|
||||
When using the "lsp" channel mode, {expr} must be a |Dict|.
|
||||
When using the "lsp" or "dap" channel mode, {expr} must be a
|
||||
|Dict|.
|
||||
|
||||
If the channel mode is "lsp", then returns a Dict. Otherwise
|
||||
returns an empty String. If the "callback" item is present in
|
||||
{options}, then the returned Dict contains the ID of the
|
||||
request message. The ID can be used to send a cancellation
|
||||
request to the LSP server (if needed). Returns an empty Dict
|
||||
on error.
|
||||
If the channel mode is "lsp" or "dap", then returns a Dict.
|
||||
Otherwise returns an empty String. If the "callback" item is
|
||||
present in {options}, then the returned Dict contains the ID
|
||||
of the request message. The ID can be used to send a
|
||||
cancellation request to the LSP server or debug adapter (if
|
||||
needed). Returns an empty Dict on error.
|
||||
|
||||
If a response message is not expected for {expr}, then don't
|
||||
specify the "callback" item in {options}.
|
||||
@@ -1607,5 +1613,33 @@ The "params" field is optional: >
|
||||
"params": <list|dict>
|
||||
}
|
||||
|
||||
<
|
||||
==============================================================================
|
||||
16. Debug Adapter Protocol *debug-adapter-protocol*
|
||||
|
||||
The debug adapter protocol is very similar to the language server protocol,
|
||||
with the main difference being that it does not use the JSON-RPC format. The
|
||||
specification can be found here:
|
||||
|
||||
https://microsoft.github.io/debug-adapter-protocol/specification
|
||||
|
||||
The protocol uses the same header format as the LSP protocol.
|
||||
|
||||
To encode and send a DAP request/notification message in a Vim |Dict| into a
|
||||
JSON message and to receive and decode a DAP JSON response/notification
|
||||
message into a Vim |Dict|, connect to the debug adapter with the
|
||||
|channel-mode| set to "dap".
|
||||
|
||||
For messages received on a channel with |channel-mode| set to "dap", Vim will
|
||||
process the HTTP header and decode the JSON payload into a Vim |Dict| type.
|
||||
When sending messages on a channel using the |ch_evalexpr()| or
|
||||
|ch_sendexpr()| functions, Vim will add the HTTP header and encode the Vim
|
||||
expression into JSON.
|
||||
|
||||
Vim will automatically add the "seq" field to the JSON DAP message, and manage
|
||||
the "request_seq" field as well for responses. However it will not add the
|
||||
"type" field, it should be manually specified in the |Dict|.
|
||||
|
||||
Otherwise the behaviour is the same as how Vim handles the "lsp" channel mode
|
||||
|language-server-protocol|.
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||
|
||||
@@ -6989,6 +6989,7 @@ dav pi_netrw.txt /*dav*
|
||||
davs pi_netrw.txt /*davs*
|
||||
daw motion.txt /*daw*
|
||||
dd change.txt /*dd*
|
||||
debug-adapter-protocol channel.txt /*debug-adapter-protocol*
|
||||
debug-gcc debug.txt /*debug-gcc*
|
||||
debug-highlight debugger.txt /*debug-highlight*
|
||||
debug-leaks debug.txt /*debug-leaks*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
*version9.txt* For Vim version 9.2. Last change: 2026 Feb 24
|
||||
*version9.txt* For Vim version 9.2. Last change: 2026 Feb 25
|
||||
|
||||
|
||||
VIM REFERENCE MANUAL by Bram Moolenaar
|
||||
@@ -52592,6 +52592,7 @@ Other ~
|
||||
-----
|
||||
- The new |xdg.vim| script for full XDG compatibility is included.
|
||||
- |ConPTY| support is considered stable as of Windows 11.
|
||||
- Support for "dap" channel mode for the |debug-adapter-protocol|.
|
||||
|
||||
*changed-9.3*
|
||||
Changed~
|
||||
|
||||
+110
-54
@@ -1981,9 +1981,9 @@ channel_collapse(channel_T *channel, ch_part_T part, int want_nl)
|
||||
|
||||
last_node = node->rq_next;
|
||||
len = node->rq_buflen + last_node->rq_buflen;
|
||||
if (want_nl || mode == CH_MODE_LSP)
|
||||
if (want_nl || mode == CH_MODE_LSP || mode == CH_MODE_DAP)
|
||||
while (last_node->rq_next != NULL
|
||||
&& (mode == CH_MODE_LSP
|
||||
&& (mode == CH_MODE_LSP || mode == CH_MODE_DAP
|
||||
|| channel_first_nl(last_node) == NULL))
|
||||
{
|
||||
last_node = last_node->rq_next;
|
||||
@@ -2134,16 +2134,22 @@ channel_fill(js_read_T *reader)
|
||||
}
|
||||
|
||||
/*
|
||||
* Process the HTTP header in a Language Server Protocol (LSP) message.
|
||||
* Process the HTTP header in a Language Server Protocol (LSP) message or
|
||||
* Debug Adapter Protocol (DAP) message.
|
||||
*
|
||||
* The message format is described in the LSP specification:
|
||||
* https://microsoft.github.io/language-server-protocol/specification
|
||||
*
|
||||
* For DAP:
|
||||
* https://microsoft.github.io/debug-adapter-protocol/specification
|
||||
*
|
||||
* It has the following two fields:
|
||||
*
|
||||
* Content-Length: ...
|
||||
* Content-Type: application/vscode-jsonrpc; charset=utf-8
|
||||
*
|
||||
* For DAP, there is no "Content-Type" field (as of now).
|
||||
*
|
||||
* Each field ends with "\r\n". The header ends with an additional "\r\n".
|
||||
*
|
||||
* Returns OK if a valid header is received and FAIL if some fields in the
|
||||
@@ -2151,7 +2157,7 @@ channel_fill(js_read_T *reader)
|
||||
* need to wait for more data to arrive.
|
||||
*/
|
||||
static int
|
||||
channel_process_lsp_http_hdr(js_read_T *reader)
|
||||
channel_process_lspdap_http_hdr(js_read_T *reader)
|
||||
{
|
||||
char_u *line_start;
|
||||
char_u *p;
|
||||
@@ -2235,8 +2241,9 @@ channel_parse_json(channel_T *channel, ch_part_T part)
|
||||
reader.js_cookie = channel;
|
||||
reader.js_cookie_arg = part;
|
||||
|
||||
if (chanpart->ch_mode == CH_MODE_LSP)
|
||||
status = channel_process_lsp_http_hdr(&reader);
|
||||
if (chanpart->ch_mode == CH_MODE_LSP
|
||||
|| chanpart->ch_mode == CH_MODE_DAP)
|
||||
status = channel_process_lspdap_http_hdr(&reader);
|
||||
|
||||
// When a message is incomplete we wait for a short while for more to
|
||||
// arrive. After the delay drop the input, otherwise a truncated string
|
||||
@@ -2253,12 +2260,13 @@ channel_parse_json(channel_T *channel, ch_part_T part)
|
||||
{
|
||||
// Only accept the response when it is a list with at least two
|
||||
// items.
|
||||
if (chanpart->ch_mode == CH_MODE_LSP && listtv.v_type != VAR_DICT)
|
||||
if ((chanpart->ch_mode == CH_MODE_LSP || chanpart->ch_mode == CH_MODE_DAP)
|
||||
&& listtv.v_type != VAR_DICT)
|
||||
{
|
||||
ch_error(channel, "Did not receive a LSP dict, discarding");
|
||||
clear_tv(&listtv);
|
||||
}
|
||||
else if (chanpart->ch_mode != CH_MODE_LSP
|
||||
else if (chanpart->ch_mode != CH_MODE_LSP && chanpart->ch_mode != CH_MODE_DAP
|
||||
&& (listtv.v_type != VAR_LIST || listtv.vval.v_list->lv_len < 2))
|
||||
{
|
||||
if (listtv.v_type != VAR_LIST)
|
||||
@@ -2467,7 +2475,7 @@ channel_has_block_id(chanpart_T *chanpart, int id)
|
||||
/*
|
||||
* Get a message from the JSON queue for channel "channel".
|
||||
* When "id" is positive it must match the first number in the list.
|
||||
* When "id" is zero or negative jut get the first message. But not one
|
||||
* When "id" is zero or negative just get the first message. But not one
|
||||
* in the ch_block_ids list.
|
||||
* When "without_callback" is TRUE also get messages that were pushed back.
|
||||
* Return OK when found and return the value in "rettv".
|
||||
@@ -2489,7 +2497,8 @@ channel_get_json(
|
||||
list_T *l;
|
||||
typval_T *tv;
|
||||
|
||||
if (channel->ch_part[part].ch_mode != CH_MODE_LSP)
|
||||
if (channel->ch_part[part].ch_mode != CH_MODE_LSP
|
||||
&& channel->ch_part[part].ch_mode != CH_MODE_DAP)
|
||||
{
|
||||
l = item->jq_value->vval.v_list;
|
||||
CHECK_LIST_MATERIALIZE(l);
|
||||
@@ -2500,29 +2509,50 @@ channel_get_json(
|
||||
dict_T *d;
|
||||
dictitem_T *di;
|
||||
|
||||
// LSP message payload is a JSON-RPC dict.
|
||||
// For RPC requests and responses, the 'id' item will be present.
|
||||
// For notifications, it will not be present.
|
||||
if (id > 0)
|
||||
if (channel->ch_part[part].ch_mode == CH_MODE_LSP)
|
||||
{
|
||||
if (item->jq_value->v_type != VAR_DICT)
|
||||
goto nextitem;
|
||||
d = item->jq_value->vval.v_dict;
|
||||
if (d == NULL)
|
||||
goto nextitem;
|
||||
// When looking for a response message from the LSP server,
|
||||
// ignore new LSP request and notification messages. LSP
|
||||
// request and notification messages have the "method" field in
|
||||
// the header and the response messages do not have this field.
|
||||
if (dict_has_key(d, "method"))
|
||||
goto nextitem;
|
||||
di = dict_find(d, (char_u *)"id", -1);
|
||||
if (di == NULL)
|
||||
goto nextitem;
|
||||
tv = &di->di_tv;
|
||||
// LSP message payload is a JSON-RPC dict. For RPC requests and
|
||||
// responses, the 'id' item will be present. For notifications,
|
||||
// it will not be present.
|
||||
if (id > 0)
|
||||
{
|
||||
if (item->jq_value->v_type != VAR_DICT)
|
||||
goto nextitem;
|
||||
d = item->jq_value->vval.v_dict;
|
||||
if (d == NULL)
|
||||
goto nextitem;
|
||||
// When looking for a response message from the LSP server,
|
||||
// ignore new LSP request and notification messages. LSP
|
||||
// request and notification messages have the "method" field
|
||||
// in the header and the response messages do not have this
|
||||
// field.
|
||||
if (dict_has_key(d, "method"))
|
||||
goto nextitem;
|
||||
di = dict_find(d, (char_u *)"id", -1);
|
||||
if (di == NULL)
|
||||
goto nextitem;
|
||||
tv = &di->di_tv;
|
||||
}
|
||||
else
|
||||
tv = item->jq_value;
|
||||
}
|
||||
else
|
||||
tv = item->jq_value;
|
||||
{
|
||||
if (id > 0)
|
||||
{
|
||||
if (item->jq_value->v_type != VAR_DICT)
|
||||
goto nextitem;
|
||||
d = item->jq_value->vval.v_dict;
|
||||
if (d == NULL)
|
||||
goto nextitem;
|
||||
di = dict_find(d, (char_u *)"request_seq", -1);
|
||||
if (di == NULL)
|
||||
goto nextitem;
|
||||
tv = &di->di_tv;
|
||||
}
|
||||
else
|
||||
tv = item->jq_value;
|
||||
}
|
||||
}
|
||||
|
||||
if ((without_callback || !item->jq_no_callback)
|
||||
@@ -2904,7 +2934,8 @@ channel_use_json_head(channel_T *channel, ch_part_T part)
|
||||
ch_mode_T ch_mode = channel->ch_part[part].ch_mode;
|
||||
|
||||
return ch_mode == CH_MODE_JSON || ch_mode == CH_MODE_JS
|
||||
|| ch_mode == CH_MODE_LSP;
|
||||
|| ch_mode == CH_MODE_LSP
|
||||
|| ch_mode == CH_MODE_DAP;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2961,10 +2992,10 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
|
||||
// Get any json message in the queue.
|
||||
if (channel_get_json(channel, part, -1, FALSE, &listtv) == FAIL)
|
||||
{
|
||||
if (ch_mode == CH_MODE_LSP)
|
||||
// In the "lsp" mode, the http header and the json payload may
|
||||
// be received in multiple messages. So concatenate all the
|
||||
// received messages.
|
||||
if (ch_mode == CH_MODE_LSP || ch_mode == CH_MODE_DAP)
|
||||
// In the "lsp" or "dap" mode, the http header and the json
|
||||
// payload may be received in multiple messages. So concatenate
|
||||
// all the received messages.
|
||||
(void)channel_collapse(channel, part, FALSE);
|
||||
|
||||
// Parse readahead, return when there is still no message.
|
||||
@@ -2973,7 +3004,7 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (ch_mode == CH_MODE_LSP)
|
||||
if (ch_mode == CH_MODE_LSP || ch_mode == CH_MODE_DAP)
|
||||
{
|
||||
dict_T *d = listtv->vval.v_dict;
|
||||
dictitem_T *di;
|
||||
@@ -2981,7 +3012,10 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
|
||||
seq_nr = 0;
|
||||
if (d != NULL)
|
||||
{
|
||||
di = dict_find(d, (char_u *)"id", -1);
|
||||
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)
|
||||
seq_nr = di->di_tv.vval.v_number;
|
||||
}
|
||||
@@ -3098,13 +3132,14 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
|
||||
called_otc = FALSE;
|
||||
if (seq_nr > 0)
|
||||
{
|
||||
// JSON or JS or LSP mode: invoke the one-time callback with the
|
||||
// JSON or JS or LSP or DAP mode: invoke the one-time callback with the
|
||||
// matching nr
|
||||
int lsp_req_msg = FALSE;
|
||||
|
||||
// Don't use a LSP server request message with the same sequence number
|
||||
// as the client request message as the response message.
|
||||
if (ch_mode == CH_MODE_LSP && argv[1].v_type == VAR_DICT
|
||||
// Don't use a LSP/DAP server request message with the same sequence
|
||||
// number as the client request message as the response message.
|
||||
if ((ch_mode == CH_MODE_LSP || ch_mode == CH_MODE_DAP)
|
||||
&& argv[1].v_type == VAR_DICT
|
||||
&& dict_has_key(argv[1].vval.v_dict, "method"))
|
||||
lsp_req_msg = TRUE;
|
||||
|
||||
@@ -3123,7 +3158,8 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
|
||||
}
|
||||
}
|
||||
|
||||
if (seq_nr > 0 && (ch_mode != CH_MODE_LSP || called_otc))
|
||||
if (seq_nr > 0 && ((ch_mode != CH_MODE_LSP && ch_mode != CH_MODE_DAP)
|
||||
|| called_otc))
|
||||
{
|
||||
if (!called_otc)
|
||||
{
|
||||
@@ -3315,6 +3351,7 @@ channel_part_info(channel_T *channel, dict_T *dict, char *name, ch_part_T part)
|
||||
case CH_MODE_JSON: s = "JSON"; break;
|
||||
case CH_MODE_JS: s = "JS"; break;
|
||||
case CH_MODE_LSP: s = "LSP"; break;
|
||||
case CH_MODE_DAP: s = "DAP"; break;
|
||||
}
|
||||
dict_add_string(dict, namebuf, (char_u *)s);
|
||||
|
||||
@@ -3982,10 +4019,10 @@ channel_read_json_block(
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (mode == CH_MODE_LSP)
|
||||
// In the "lsp" mode, the http header and the json payload may be
|
||||
// received in multiple messages. So concatenate all the received
|
||||
// messages.
|
||||
if (mode == CH_MODE_LSP || mode == CH_MODE_DAP)
|
||||
// In the "lsp" or "dap" mode, the http header and the json payload
|
||||
// may be received in multiple messages. So concatenate all the
|
||||
// received messages.
|
||||
(void)channel_collapse(channel, part, FALSE);
|
||||
|
||||
more = channel_parse_json(channel, part);
|
||||
@@ -4558,7 +4595,7 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch_mode == CH_MODE_LSP)
|
||||
if (ch_mode == CH_MODE_LSP || ch_mode == CH_MODE_DAP)
|
||||
{
|
||||
dict_T *d;
|
||||
dictitem_T *di;
|
||||
@@ -4571,11 +4608,15 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
|
||||
return;
|
||||
|
||||
d = argvars[1].vval.v_dict;
|
||||
di = dict_find(d, (char_u *)"id", -1);
|
||||
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)
|
||||
{
|
||||
// only number type is supported for the 'id' item
|
||||
semsg(_(e_invalid_value_for_argument_str), "id");
|
||||
// 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");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4583,7 +4624,16 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
|
||||
if (dict_has_key(argvars[2].vval.v_dict, "callback"))
|
||||
callback_present = TRUE;
|
||||
|
||||
if (eval || callback_present)
|
||||
if (ch_mode == CH_MODE_DAP)
|
||||
{
|
||||
// DAP message always has a sequence number (id)
|
||||
id = ++channel->ch_last_msg_id;
|
||||
if (di == NULL)
|
||||
dict_add_number(d, "seq", id);
|
||||
else
|
||||
di->di_tv.vval.v_number = id;
|
||||
}
|
||||
else if (eval || callback_present)
|
||||
{
|
||||
// When evaluating an expression or sending an expression with a
|
||||
// callback, always assign a generated ID
|
||||
@@ -4601,7 +4651,7 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
|
||||
if (di != NULL)
|
||||
id = di->di_tv.vval.v_number;
|
||||
}
|
||||
if (!dict_has_key(d, "jsonrpc"))
|
||||
if (ch_mode == CH_MODE_LSP && !dict_has_key(d, "jsonrpc"))
|
||||
dict_add_string(d, "jsonrpc", (char_u *)"2.0");
|
||||
text = json_encode_lsp_msg(&argvars[1]);
|
||||
}
|
||||
@@ -4626,7 +4676,7 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
|
||||
if (channel_read_json_block(channel, part_read, timeout, id, &listtv)
|
||||
== OK)
|
||||
{
|
||||
if (ch_mode == CH_MODE_LSP)
|
||||
if (ch_mode == CH_MODE_LSP || ch_mode == CH_MODE_DAP)
|
||||
{
|
||||
*rettv = *listtv;
|
||||
// Change the type to avoid the value being freed.
|
||||
@@ -4646,7 +4696,13 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int eval)
|
||||
}
|
||||
}
|
||||
free_job_options(&opt);
|
||||
if (ch_mode == CH_MODE_LSP && !eval && callback_present)
|
||||
if (ch_mode == CH_MODE_DAP && !eval)
|
||||
{
|
||||
// A DAP message always has a sequence number.
|
||||
if (rettv->vval.v_dict != NULL)
|
||||
dict_add_number(rettv->vval.v_dict, "seq", id);
|
||||
}
|
||||
else if (ch_mode == CH_MODE_LSP && !eval && callback_present)
|
||||
{
|
||||
// if ch_sendexpr() is used to send a LSP message and a callback
|
||||
// function is specified, then return the generated identifier for the
|
||||
|
||||
@@ -33,6 +33,8 @@ handle_mode(typval_T *item, jobopt_T *opt, ch_mode_T *modep, int jo)
|
||||
*modep = CH_MODE_JSON;
|
||||
else if (STRCMP(val, "lsp") == 0)
|
||||
*modep = CH_MODE_LSP;
|
||||
else if (STRCMP(val, "dap") == 0)
|
||||
*modep = CH_MODE_DAP;
|
||||
else
|
||||
{
|
||||
semsg(_(e_invalid_argument_str), val);
|
||||
|
||||
+3
-1
@@ -2599,7 +2599,9 @@ typedef enum
|
||||
CH_MODE_RAW,
|
||||
CH_MODE_JSON,
|
||||
CH_MODE_JS,
|
||||
CH_MODE_LSP // Language Server Protocol (http + json)
|
||||
CH_MODE_LSP, // Language Server Protocol (http + json)
|
||||
CH_MODE_DAP // Debug Adapter Protocol (like LSP, but does not
|
||||
// strictly follow JSON-RPC standard)
|
||||
} ch_mode_T;
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -2764,6 +2764,64 @@ func Test_channel_lsp_mode()
|
||||
call RunServer('test_channel_lsp.py', 'LspTests', [])
|
||||
endfunc
|
||||
|
||||
" Test for the 'dap' channel mode. Don't need to test much since most of the
|
||||
" logic is same as 'lsp' mode.
|
||||
func DapTests(port)
|
||||
let ch = ch_open(s:localhost .. a:port, #{
|
||||
\ mode: 'dap',
|
||||
\ })
|
||||
|
||||
if ch_status(ch) == "fail"
|
||||
call assert_report("Can't open the dap channel")
|
||||
return
|
||||
endif
|
||||
|
||||
" check for channel information
|
||||
let info = ch_info(ch)
|
||||
call assert_equal('DAP', info.sock_mode)
|
||||
|
||||
let resp = ch_evalexpr(ch, #{
|
||||
\ type: 'request',
|
||||
\ command: 'initialize'
|
||||
\ })
|
||||
call assert_equal({
|
||||
\ 'seq': 1,
|
||||
\ 'request_seq': 1,
|
||||
\ 'type': 'response',
|
||||
\ 'success': v:true,
|
||||
\ 'body': {'supportsConfigurationDoneRequest': v:true},
|
||||
\ 'command': 'initialize'
|
||||
\ }, resp)
|
||||
|
||||
let resp = ch_read(ch)
|
||||
|
||||
call assert_equal({
|
||||
\ 'seq': 2,
|
||||
\ 'type': 'event',
|
||||
\ 'event': 'initialized',
|
||||
\ 'body': {}
|
||||
\ }, resp)
|
||||
|
||||
let resp = ch_evalexpr(ch, #{
|
||||
\ type: 'request',
|
||||
\ command: 'test'
|
||||
\ })
|
||||
|
||||
call assert_equal({
|
||||
\ 'seq': 3,
|
||||
\ 'request_seq': 2,
|
||||
\ 'type': 'response',
|
||||
\ 'success': v:true,
|
||||
\ 'body': {},
|
||||
\ 'command': 'test'
|
||||
\ }, resp)
|
||||
endfunc
|
||||
|
||||
func Test_channel_dap_mode()
|
||||
let g:giveup_same_error = 0
|
||||
call RunServer('test_channel_dap.py', 'DapTests', [])
|
||||
endfunc
|
||||
|
||||
func Test_error_callback_terminal()
|
||||
CheckUnix
|
||||
CheckFeature terminal
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Used by Test_channel_dap_mode in test_channel.vim to test DAP functionality.
|
||||
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
import socketserver
|
||||
except ImportError:
|
||||
import SocketServer as socketserver
|
||||
|
||||
def make_dap_message(obj):
|
||||
payload = json.dumps(obj).encode("utf-8")
|
||||
header = f"Content-Length: {len(payload)}\r\n\r\n".encode("ascii")
|
||||
return header + payload
|
||||
|
||||
|
||||
def parse_messages(buffer):
|
||||
messages = []
|
||||
|
||||
while True:
|
||||
hdr_end = buffer.find(b"\r\n\r\n")
|
||||
if hdr_end == -1:
|
||||
break
|
||||
|
||||
header = buffer[:hdr_end].decode("ascii", errors="ignore")
|
||||
content_length = None
|
||||
|
||||
for line in header.split("\r\n"):
|
||||
if line.lower().startswith("content-length:"):
|
||||
content_length = int(line.split(":")[1].strip())
|
||||
|
||||
if content_length is None:
|
||||
break
|
||||
|
||||
total_len = hdr_end + 4 + content_length
|
||||
if len(buffer) < total_len:
|
||||
break # partial
|
||||
|
||||
body = buffer[hdr_end + 4:total_len]
|
||||
messages.append(json.loads(body.decode("utf-8")))
|
||||
buffer = buffer[total_len:]
|
||||
|
||||
return messages, buffer
|
||||
|
||||
|
||||
class DAPHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def setup(self):
|
||||
self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.seq = 1 # server sequence counter
|
||||
|
||||
def send(self, obj):
|
||||
obj["seq"] = self.seq
|
||||
self.seq += 1
|
||||
self.request.sendall(make_dap_message(obj))
|
||||
|
||||
def send_response(self, request, body=None, success=True):
|
||||
self.send({
|
||||
"type": "response",
|
||||
"request_seq": request["seq"],
|
||||
"success": success,
|
||||
"command": request["command"],
|
||||
"body": body or {}
|
||||
})
|
||||
|
||||
def send_event(self, event, body=None):
|
||||
self.send({
|
||||
"type": "event",
|
||||
"event": event,
|
||||
"body": body or {}
|
||||
})
|
||||
|
||||
def handle_request(self, msg):
|
||||
cmd = msg.get("command")
|
||||
|
||||
if cmd == "initialize":
|
||||
self.send_response(msg, {
|
||||
"supportsConfigurationDoneRequest": True
|
||||
})
|
||||
self.send_event("initialized")
|
||||
else:
|
||||
self.send_response(msg)
|
||||
|
||||
return True
|
||||
|
||||
def handle(self):
|
||||
buffer = b""
|
||||
|
||||
while True:
|
||||
data = self.request.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
|
||||
buffer += data
|
||||
messages, buffer = parse_messages(buffer)
|
||||
|
||||
for msg in messages:
|
||||
if msg.get("type") == "request":
|
||||
if not self.handle_request(msg):
|
||||
return
|
||||
|
||||
|
||||
class ThreadedDAPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def write_port_to_file(port, filename="Xportnr"):
|
||||
with open(filename, "w") as f:
|
||||
f.write(str(port))
|
||||
|
||||
def main():
|
||||
server = ThreadedDAPServer(("localhost", 0), DAPHandler)
|
||||
|
||||
# Get the actual assigned port
|
||||
ip, assigned_port = server.server_address
|
||||
|
||||
# Write port so client/test can read it
|
||||
write_port_to_file(assigned_port)
|
||||
|
||||
thread = threading.Thread(target=server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
try:
|
||||
while thread.is_alive():
|
||||
thread.join(1)
|
||||
except KeyboardInterrupt:
|
||||
server.shutdown()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -734,6 +734,8 @@ static char *(features[]) =
|
||||
|
||||
static int included_patches[] =
|
||||
{ /* Add new patch number below this line */
|
||||
/**/
|
||||
60,
|
||||
/**/
|
||||
59,
|
||||
/**/
|
||||
|
||||
Reference in New Issue
Block a user