-*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
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}
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
"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
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
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
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.
"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
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}.
"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:
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*
-*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
-----
- 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~
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;
}
/*
- * 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
* 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;
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
{
// 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)
/*
* 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".
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);
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)
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;
}
/*
// 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.
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;
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;
}
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;
}
}
- 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)
{
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);
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);
return;
}
- if (ch_mode == CH_MODE_LSP)
+ if (ch_mode == CH_MODE_LSP || ch_mode == CH_MODE_DAP)
{
dict_T *d;
dictitem_T *di;
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;
}
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
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]);
}
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.
}
}
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
*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);
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 {
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
--- /dev/null
+#!/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()
+
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 60,
/**/
59,
/**/