To: vim_dev@googlegroups.com Subject: Patch 8.2.4648 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 8.2.4648 Problem: Handling LSP messages is a bit slow. Solution: Included support for LSP messages. (Yegappan Lakshmanan, closes #10025) Files: runtime/doc/channel.txt, src/channel.c, src/job.c, src/json.c, src/proto/json.pro, src/structs.h, src/testdir/test_channel.vim, src/testdir/test_channel_lsp.py *** ../vim-8.2.4647/runtime/doc/channel.txt 2021-07-15 11:48:08.803766853 +0100 --- runtime/doc/channel.txt 2022-03-30 10:03:09.018655929 +0100 *************** *** 53,58 **** --- 53,59 ---- 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| Common combination are: - Using a job connected through pipes in NL mode. E.g., to run a style *************** *** 91,97 **** To handle asynchronous communication a callback needs to be used: > func MyHandler(channel, msg) ! echo "from the handler: " . a:msg endfunc call ch_sendexpr(channel, 'hello!', {'callback': "MyHandler"}) Vim will not wait for a response. Now the server can send the response later --- 92,98 ---- To handle asynchronous communication a callback needs to be used: > func MyHandler(channel, msg) ! echo "from the handler: " .. a:msg endfunc call ch_sendexpr(channel, 'hello!', {'callback': "MyHandler"}) Vim will not wait for a response. Now the server can send the response later *************** *** 101,107 **** when opening the channel: > call ch_close(channel) let channel = ch_open('localhost:8765', {'callback': "MyHandler"}) ! call ch_sendexpr(channel, 'hello!') When trying out channels it's useful to see what is going on. You can tell Vim to write lines in log file: > --- 102,108 ---- when opening the channel: > call ch_close(channel) let channel = ch_open('localhost:8765', {'callback': "MyHandler"}) ! call ch_sendexpr(channel, 'hello channel!') When trying out channels it's useful to see what is going on. You can tell Vim to write lines in log file: > *************** *** 130,146 **** "js" - Use JS (JavaScript) encoding, more efficient than JSON. "nl" - Use messages that end in a NL character "raw" - Use raw messages *channel-callback* *E921* "callback" A function that is called when a message is received that is ! not handled otherwise. It gets two arguments: the channel ! and the received message. Example: > func Handle(channel, msg) ! echo 'Received: ' . a:msg endfunc let channel = ch_open("localhost:8765", {"callback": "Handle"}) < ! When "mode" is "json" or "js" 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 --- 131,149 ---- "js" - Use JS (JavaScript) encoding, more efficient than JSON. "nl" - Use messages that end in a NL character "raw" - Use raw messages + "lsp" - Use language server 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 ! gets two arguments: the channel and the received message. ! Example: > func Handle(channel, msg) ! echo 'Received: ' .. a:msg 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 "nl" the "msg" argument is one message, excluding the NL. When "mode" is "raw" the "msg" argument is the whole message *************** *** 164,170 **** to check for messages, the close_cb may be invoked while still in the callback. The plugin must handle this somehow, it can be useful to know that no more data is coming. ! *channel-drop* "drop" Specifies when to drop messages: "auto" When there is no callback to handle a message. The "close_cb" is also considered for this. --- 167,185 ---- to check for messages, the close_cb may be invoked while still in the callback. The plugin must handle this somehow, it can be useful to know that no more data is coming. ! If it is not known if there is a message to be read, use a ! try/catch block: > ! try ! let msg = ch_readraw(a:channel) ! catch ! let msg = 'no message' ! endtry ! try ! let err = ch_readraw(a:channel, #{part: 'err'}) ! catch ! let err = 'no error' ! endtry ! < *channel-drop* "drop" Specifies when to drop messages: "auto" When there is no callback to handle a message. The "close_cb" is also considered for this. *************** *** 442,448 **** Note that when there is no callback, messages are dropped. To avoid that add a close callback to the channel. ! To read all output from a RAW channel that is available: > let output = ch_readraw(channel) To read the error output: > let output = ch_readraw(channel, {"part": "err"}) --- 457,463 ---- Note that when there is no callback, messages are dropped. To avoid that add a close callback to the channel. ! To read all normal output from a RAW channel that is available: > let output = ch_readraw(channel) To read the error output: > let output = ch_readraw(channel, {"part": "err"}) *************** *** 502,507 **** --- 517,523 ---- 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|. *E917* {options} must be a Dictionary. It must not have a "callback" entry. It can have a "timeout" entry to specify the timeout *************** *** 536,542 **** GetChannel()->ch_evalraw(rawstring) ch_getbufnr({handle}, {what}) *ch_getbufnr()* ! Get the buffer number that {handle} is using for {what}. {handle} can be a Channel or a Job that has a Channel. {what} can be "err" for stderr, "out" for stdout or empty for socket output. --- 552,558 ---- GetChannel()->ch_evalraw(rawstring) ch_getbufnr({handle}, {what}) *ch_getbufnr()* ! Get the buffer number that {handle} is using for String {what}. {handle} can be a Channel or a Job that has a Channel. {what} can be "err" for stderr, "out" for stdout or empty for socket output. *************** *** 577,583 **** "err_io" "out", "null", "pipe", "file" or "buffer" "err_timeout" timeout in msec "in_status" "open" or "closed" ! "in_mode" "NL", "RAW", "JSON" or "JS" "in_io" "null", "pipe", "file" or "buffer" "in_timeout" timeout in msec --- 593,599 ---- "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_io" "null", "pipe", "file" or "buffer" "in_timeout" timeout in msec *************** *** 586,593 **** ch_log({msg} [, {handle}]) *ch_log()* ! Write {msg} in the channel log file, if it was opened with ! |ch_logfile()|. When {handle} is passed the channel number is used for the message. {handle} can be a Channel or a Job that has a Channel. The --- 602,609 ---- ch_log({msg} [, {handle}]) *ch_log()* ! Write String {msg} in the channel log file, if it was opened ! with |ch_logfile()|. When {handle} is passed the channel number is used for the message. {handle} can be a Channel or a Job that has a Channel. The *************** *** 625,631 **** Open a channel to {address}. See |channel|. Returns a Channel. Use |ch_status()| to check for failure. ! {address} has the form "hostname:port", e.g., "localhost:8765". When using an IPv6 address, enclose it within square brackets. --- 641,647 ---- Open a channel to {address}. See |channel|. Returns a Channel. Use |ch_status()| to check for failure. ! {address} is a String and has the form "hostname:port", e.g., "localhost:8765". When using an IPv6 address, enclose it within square brackets. *************** *** 673,678 **** --- 689,695 ---- 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|. Can also be used as a |method|: > GetChannel()->ch_sendexpr(expr) *************** *** 1287,1304 **** " Create a channel log so we can see what happens. call ch_logfile('logfile', 'w') ! " Function handling a line of text has been typed. func TextEntered(text) " Send the text to a shell with Enter appended. call ch_sendraw(g:shell_job, a:text .. "\n") endfunc ! " Function handling output from the shell: Added above the prompt. func GotOutput(channel, msg) ! call append(line("$") - 1, "- " . a:msg) endfunc ! " Function handling the shell exist: close the window. func JobExit(job, status) quit! endfunc --- 1304,1321 ---- " Create a channel log so we can see what happens. call ch_logfile('logfile', 'w') ! " Function handling a line of text that has been typed. func TextEntered(text) " Send the text to a shell with Enter appended. call ch_sendraw(g:shell_job, a:text .. "\n") endfunc ! " Function handling output from the shell: Add it above the prompt. func GotOutput(channel, msg) ! call append(line("$") - 1, "- " .. a:msg) endfunc ! " Function handling the shell exits: close the window. func JobExit(job, status) quit! endfunc *************** *** 1309,1315 **** \ err_cb: function('GotOutput'), \ exit_cb: function('JobExit'), \ }) - let shell_ch = job_getchannel(shell_job) new set buftype=prompt --- 1326,1331 ---- *************** *** 1320,1325 **** --- 1336,1452 ---- " start accepting shell commands startinsert < + The same in |Vim9| script: > + vim9script + + # Create a channel log so we can see what happens. + ch_logfile('logfile', 'w') + + var shell_job: job + + # Function handling a line of text that has been typed. + def TextEntered(text: string) + # Send the text to a shell with Enter appended. + ch_sendraw(shell_job, text .. "\n") + enddef + + # Function handling output from the shell: Add it above the prompt. + def GotOutput(channel: channel, msg: string) + append(line("$") - 1, "- " .. msg) + enddef + + # Function handling the shell exits: close the window. + def JobExit(job: job, status: number) + quit! + enddef + + # Start a shell in the background. + shell_job = job_start(["/bin/sh"], { + out_cb: GotOutput, + err_cb: GotOutput, + exit_cb: JobExit, + }) + + new + set buftype=prompt + var buf = bufnr('') + prompt_setcallback(buf, TextEntered) + prompt_setprompt(buf, "shell command: ") + + # start accepting shell commands + startinsert + + ============================================================================== + 14. Language Server Protocol *language-server-protocol* + + The language server protocol specification is available at: + + https://microsoft.github.io/language-server-protocol/specification + + Each LSP protocol message starts with a simple HTTP header followed by the + payload encoded in JSON-RPC format. This is described in: + + https://www.jsonrpc.org/specification + + For messages received on a channel with mode set to "lsp", Vim will process + the HTTP header and decode the payload into a Vim |Dict| type and call the + channel callback or the specified callback function. When sending messages on + a channel using |ch_evalexpr()| or |ch_sendexpr()|, Vim will add the HTTP + header and encode the Vim expression into JSON-RPC. + + To open a channel using the 'lsp' mode, set the 'mode' item in the |ch_open()| + {options} argument to 'lsp'. Example: > + + let ch = ch_open(..., #{mode: 'lsp'}) + + To open a channel using the 'lsp' mode with a job, set the 'in_mode' and + 'out_mode' items in the |job_start()| {options} argument to 'lsp'. Example: > + + let job = job_start(...., #{in_mode: 'lsp', out_mode: 'lsp'}) + + To synchronously send a JSON-RPC request to the server, use the |ch_evalexpr()| + function. This function will return the response from the server. You can use + the 'timeout' field in the {options} argument to control the response wait + time. Example: > + + let req = {} + let req.method = 'textDocument/definition' + let req.params = {} + let req.params.textDocument = #{uri: 'a.c'} + let req.params.position = #{line: 10, character: 3} + let resp = ch_evalexpr(ch, req, #{timeout: 100}) + + Note that in the request message the 'id' field should not be specified. If it + is specified, then Vim will overwrite the value with an internally generated + identifier. Vim currently supports only a number type for the 'id' field. + + To send a JSON-RPC request to the server and asynchronously process the + response, use the |ch_sendexpr()| function and supply a callback function. + Example: > + + let req = {} + let req.method = 'textDocument/hover' + let req.params = {} + let req.params.textDocument = #{uri: 'a.c'} + let req.params.position = #{line: 10, character: 3} + let resp = ch_sendexpr(ch, req, #{callback: 'MyFn'}) + + To send a JSON-RPC notification message to the server, use the |ch_sendexpr()| + function. Example: > + + call ch_sendexpr(ch, #{method: 'initialized'}) + + To respond to a JSON-RPC request message from the server, use the + |ch_sendexpr()| function. In the response message, copy the 'id' field value + from the server request message. Example: > + + let resp = {} + let resp.id = req.id + let resp.result = 1 + call ch_sendexpr(ch, resp) + + The JSON-RPC notification messages from the server are delivered through the + |channel-callback| function. vim:tw=78:ts=8:noet:ft=help:norl: *** ../vim-8.2.4647/src/channel.c 2022-01-28 15:28:00.200927841 +0000 --- src/channel.c 2022-03-30 10:03:09.022655971 +0100 *************** *** 2112,2117 **** --- 2112,2194 ---- } /* + * Process the HTTP header in a Language Server Protocol (LSP) message. + * + * The message format is described in the LSP specification: + * https://microsoft.github.io/language-server-protocol/specification + * + * It has the following two fields: + * + * Content-Length: ... + * Content-Type: application/vscode-jsonrpc; charset=utf-8 + * + * 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 + * header are not correct. Returns MAYBE if a partial header is received and + * need to wait for more data to arrive. + */ + static int + channel_process_lsp_http_hdr(js_read_T *reader) + { + char_u *line_start; + char_u *p; + int_u hdr_len; + int payload_len = -1; + int_u jsbuf_len; + + // We find the end once, to avoid calling strlen() many times. + jsbuf_len = (int_u)STRLEN(reader->js_buf); + reader->js_end = reader->js_buf + jsbuf_len; + + p = reader->js_buf; + + // Process each line in the header till an empty line is read (header + // separator). + while (TRUE) + { + line_start = p; + while (*p != NUL && *p != '\n') + p++; + if (*p == NUL) // partial header + return MAYBE; + p++; + + // process the content length field (if present) + if ((p - line_start > 16) + && STRNICMP(line_start, "Content-Length: ", 16) == 0) + { + errno = 0; + payload_len = strtol((char *)line_start + 16, NULL, 10); + if (errno == ERANGE || payload_len < 0) + // invalid length, discard the payload + return FAIL; + } + + if ((p - line_start) == 2 && line_start[0] == '\r' && + line_start[1] == '\n') + // reached the empty line + break; + } + + if (payload_len == -1) + // Content-Length field is not present in the header + return FAIL; + + hdr_len = p - reader->js_buf; + + // if the entire payload is not received, wait for more data to arrive + if (jsbuf_len < hdr_len + payload_len) + return MAYBE; + + reader->js_used += hdr_len; + // recalculate the end based on the length read from the header. + reader->js_end = reader->js_buf + hdr_len + payload_len; + + return OK; + } + + /* * Use the read buffer of "channel"/"part" and parse a JSON message that is * complete. The messages are added to the queue. * Return TRUE if there is more to read. *************** *** 2124,2130 **** jsonq_T *item; chanpart_T *chanpart = &channel->ch_part[part]; jsonq_T *head = &chanpart->ch_json_head; ! int status; int ret; if (channel_peek(channel, part) == NULL) --- 2201,2207 ---- jsonq_T *item; chanpart_T *chanpart = &channel->ch_part[part]; jsonq_T *head = &chanpart->ch_json_head; ! int status = OK; int ret; if (channel_peek(channel, part) == NULL) *************** *** 2136,2154 **** reader.js_cookie = channel; reader.js_cookie_arg = part; // 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 // or list will make us hang. // Do not generate error messages, they will be written in a channel log. ! ++emsg_silent; ! status = json_decode(&reader, &listtv, ! chanpart->ch_mode == MODE_JS ? JSON_JS : 0); ! --emsg_silent; if (status == OK) { // Only accept the response when it is a list with at least two // items. ! if (listtv.v_type != VAR_LIST || listtv.vval.v_list->lv_len < 2) { if (listtv.v_type != VAR_LIST) ch_error(channel, "Did not receive a list, discarding"); --- 2213,2243 ---- reader.js_cookie = channel; reader.js_cookie_arg = part; + if (chanpart->ch_mode == MODE_LSP) + status = channel_process_lsp_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 // or list will make us hang. // Do not generate error messages, they will be written in a channel log. ! if (status == OK) ! { ! ++emsg_silent; ! status = json_decode(&reader, &listtv, ! chanpart->ch_mode == MODE_JS ? JSON_JS : 0); ! --emsg_silent; ! } if (status == OK) { // Only accept the response when it is a list with at least two // items. ! if (chanpart->ch_mode == MODE_LSP && listtv.v_type != VAR_DICT) ! { ! ch_error(channel, "Did not receive a LSP dict, discarding"); ! clear_tv(&listtv); ! } ! else if (chanpart->ch_mode != MODE_LSP && ! (listtv.v_type != VAR_LIST || listtv.vval.v_list->lv_len < 2)) { if (listtv.v_type != VAR_LIST) ch_error(channel, "Did not receive a list, discarding"); *************** *** 2375,2385 **** while (item != NULL) { ! list_T *l = item->jq_value->vval.v_list; typval_T *tv; ! CHECK_LIST_MATERIALIZE(l); ! tv = &l->lv_first->li_tv; if ((without_callback || !item->jq_no_callback) && ((id > 0 && tv->v_type == VAR_NUMBER && tv->vval.v_number == id) --- 2464,2501 ---- while (item != NULL) { ! list_T *l; typval_T *tv; ! if (channel->ch_part[part].ch_mode != MODE_LSP) ! { ! l = item->jq_value->vval.v_list; ! CHECK_LIST_MATERIALIZE(l); ! tv = &l->lv_first->li_tv; ! } ! else ! { ! 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 (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 *)"id", -1); ! if (di == NULL) ! goto nextitem; ! tv = &di->di_tv; ! } ! else ! tv = item->jq_value; ! } if ((without_callback || !item->jq_no_callback) && ((id > 0 && tv->v_type == VAR_NUMBER && tv->vval.v_number == id) *************** *** 2395,2400 **** --- 2511,2517 ---- remove_json_node(head, item); return OK; } + nextitem: item = item->jq_next; } return FAIL; *************** *** 2762,2767 **** --- 2879,2885 ---- callback_T *callback = NULL; buf_T *buffer = NULL; char_u *p; + int called_otc; // one time callbackup if (channel->ch_nb_close_cb != NULL) // this channel is handled elsewhere (netbeans) *************** *** 2788,2794 **** buffer = NULL; } ! if (ch_mode == MODE_JSON || ch_mode == MODE_JS) { listitem_T *item; int argc = 0; --- 2906,2912 ---- buffer = NULL; } ! if (ch_mode == MODE_JSON || ch_mode == MODE_JS || ch_mode == MODE_LSP) { listitem_T *item; int argc = 0; *************** *** 2802,2830 **** return FALSE; } ! for (item = listtv->vval.v_list->lv_first; ! item != NULL && argc < CH_JSON_MAX_ARGS; ! item = item->li_next) ! argv[argc++] = item->li_tv; ! while (argc < CH_JSON_MAX_ARGS) ! argv[argc++].v_type = VAR_UNKNOWN; ! ! if (argv[0].v_type == VAR_STRING) ! { ! // ["cmd", arg] or ["cmd", arg, arg] or ["cmd", arg, arg, arg] ! channel_exe_cmd(channel, part, argv); ! free_tv(listtv); ! return TRUE; ! } ! if (argv[0].v_type != VAR_NUMBER) { ! ch_error(channel, ! "Dropping message with invalid sequence number type"); ! free_tv(listtv); ! return FALSE; } - seq_nr = argv[0].vval.v_number; } else if (channel_peek(channel, part) == NULL) { --- 2920,2966 ---- return FALSE; } ! if (ch_mode == MODE_LSP) ! { ! 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 (di != NULL && di->di_tv.v_type == VAR_NUMBER) ! seq_nr = di->di_tv.vval.v_number; ! } ! ! argv[1] = *listtv; ! } ! else { ! for (item = listtv->vval.v_list->lv_first; ! item != NULL && argc < CH_JSON_MAX_ARGS; ! item = item->li_next) ! argv[argc++] = item->li_tv; ! while (argc < CH_JSON_MAX_ARGS) ! argv[argc++].v_type = VAR_UNKNOWN; ! ! if (argv[0].v_type == VAR_STRING) ! { ! // ["cmd", arg] or ["cmd", arg, arg] or ["cmd", arg, arg, arg] ! channel_exe_cmd(channel, part, argv); ! free_tv(listtv); ! return TRUE; ! } ! ! if (argv[0].v_type != VAR_NUMBER) ! { ! ch_error(channel, ! "Dropping message with invalid sequence number type"); ! free_tv(listtv); ! return FALSE; ! } ! seq_nr = argv[0].vval.v_number; } } else if (channel_peek(channel, part) == NULL) { *************** *** 2906,2929 **** argv[1].vval.v_string = msg; } if (seq_nr > 0) { ! int done = FALSE; ! ! // JSON or JS mode: invoke the one-time callback with the matching nr for (cbitem = cbhead->cq_next; cbitem != NULL; cbitem = cbitem->cq_next) if (cbitem->cq_seq_nr == seq_nr) { invoke_one_time_callback(channel, cbhead, cbitem, argv); ! done = TRUE; break; } ! if (!done) { if (channel->ch_drop_never) { // message must be read with ch_read() channel_push_json(channel, part, listtv); listtv = NULL; } else --- 3042,3076 ---- argv[1].vval.v_string = msg; } + called_otc = FALSE; if (seq_nr > 0) { ! // JSON or JS or LSP mode: invoke the one-time callback with the ! // matching nr for (cbitem = cbhead->cq_next; cbitem != NULL; cbitem = cbitem->cq_next) if (cbitem->cq_seq_nr == seq_nr) { invoke_one_time_callback(channel, cbhead, cbitem, argv); ! called_otc = TRUE; break; } ! } ! ! if (seq_nr > 0 && (ch_mode != MODE_LSP || called_otc)) ! { ! if (!called_otc) { + // If the 'drop' channel attribute is set to 'never' or if + // ch_evalexpr() is waiting for this response message, then don't + // drop this message. if (channel->ch_drop_never) { // message must be read with ch_read() channel_push_json(channel, part, listtv); + + // Change the type to avoid the value being freed. + listtv->v_type = VAR_NUMBER; + free_tv(listtv); listtv = NULL; } else *************** *** 3006,3012 **** { ch_mode_T ch_mode = channel->ch_part[part].ch_mode; ! if (ch_mode == MODE_JSON || ch_mode == MODE_JS) { jsonq_T *head = &channel->ch_part[part].ch_json_head; --- 3153,3159 ---- { ch_mode_T ch_mode = channel->ch_part[part].ch_mode; ! if (ch_mode == MODE_JSON || ch_mode == MODE_JS || ch_mode == MODE_LSP) { jsonq_T *head = &channel->ch_part[part].ch_json_head; *************** *** 3092,3097 **** --- 3239,3245 ---- case MODE_RAW: s = "RAW"; break; case MODE_JSON: s = "JSON"; break; case MODE_JS: s = "JS"; break; + case MODE_LSP: s = "LSP"; break; } dict_add_string(dict, namebuf, (char_u *)s); *************** *** 4291,4299 **** return; } ! id = ++channel->ch_last_msg_id; ! text = json_encode_nr_expr(id, &argvars[1], ! (ch_mode == MODE_JS ? JSON_JS : 0) | JSON_NL); if (text == NULL) return; --- 4439,4497 ---- return; } ! if (ch_mode == MODE_LSP) ! { ! dict_T *d; ! dictitem_T *di; ! int callback_present = FALSE; ! ! if (argvars[1].v_type != VAR_DICT) ! { ! semsg(_(e_dict_required_for_argument_nr), 2); ! return; ! } ! d = argvars[1].vval.v_dict; ! di = dict_find(d, (char_u *)"id", -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"); ! return; ! } ! ! if (argvars[2].v_type == VAR_DICT) ! if (dict_find(argvars[2].vval.v_dict, (char_u *)"callback", -1) ! != NULL) ! callback_present = TRUE; ! ! if (eval || callback_present) ! { ! // When evaluating an expression or sending an expression with a ! // callback, always assign a generated ID ! id = ++channel->ch_last_msg_id; ! if (di == NULL) ! dict_add_number(d, "id", id); ! else ! di->di_tv.vval.v_number = id; ! } ! else ! { ! // When sending an expression, if the message has an 'id' item, ! // then use it. ! id = 0; ! if (di != NULL) ! id = di->di_tv.vval.v_number; ! } ! if (dict_find(d, (char_u *)"jsonrpc", -1) == NULL) ! dict_add_string(d, "jsonrpc", (char_u *)"2.0"); ! text = json_encode_lsp_msg(&argvars[1]); ! } ! else ! { ! id = ++channel->ch_last_msg_id; ! text = json_encode_nr_expr(id, &argvars[1], ! (ch_mode == MODE_JS ? JSON_JS : 0) | JSON_NL); ! } if (text == NULL) return; *************** *** 4309,4321 **** if (channel_read_json_block(channel, part_read, timeout, id, &listtv) == OK) { ! list_T *list = listtv->vval.v_list; ! // Move the item from the list and then change the type to ! // avoid the value being freed. ! *rettv = list->lv_u.mat.lv_last->li_tv; ! list->lv_u.mat.lv_last->li_tv.v_type = VAR_NUMBER; ! free_tv(listtv); } } free_job_options(&opt); --- 4507,4529 ---- if (channel_read_json_block(channel, part_read, timeout, id, &listtv) == OK) { ! if (ch_mode == MODE_LSP) ! { ! *rettv = *listtv; ! // Change the type to avoid the value being freed. ! listtv->v_type = VAR_NUMBER; ! free_tv(listtv); ! } ! else ! { ! list_T *list = listtv->vval.v_list; ! // Move the item from the list and then change the type to ! // avoid the value being freed. ! *rettv = list->lv_u.mat.lv_last->li_tv; ! list->lv_u.mat.lv_last->li_tv.v_type = VAR_NUMBER; ! free_tv(listtv); ! } } } free_job_options(&opt); *** ../vim-8.2.4647/src/job.c 2022-01-08 16:19:18.505639885 +0000 --- src/job.c 2022-03-30 10:03:09.022655971 +0100 *************** *** 31,36 **** --- 31,38 ---- *modep = MODE_JS; else if (STRCMP(val, "json") == 0) *modep = MODE_JSON; + else if (STRCMP(val, "lsp") == 0) + *modep = MODE_LSP; else { semsg(_(e_invalid_argument_str), val); *** ../vim-8.2.4647/src/json.c 2022-01-28 15:28:00.208927722 +0000 --- src/json.c 2022-03-30 10:03:09.022655971 +0100 *************** *** 86,91 **** --- 86,117 ---- ga_append(&ga, NUL); return ga.ga_data; } + + /* + * Encode "val" into a JSON format string prefixed by the LSP HTTP header. + * Returns NULL when out of memory. + */ + char_u * + json_encode_lsp_msg(typval_T *val) + { + garray_T ga; + garray_T lspga; + + ga_init2(&ga, 1, 4000); + if (json_encode_gap(&ga, val, 0) == FAIL) + return NULL; + ga_append(&ga, NUL); + + ga_init2(&lspga, 1, 4000); + vim_snprintf((char *)IObuff, IOSIZE, + "Content-Length: %u\r\n" + "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n\r\n", + ga.ga_len - 1); + ga_concat(&lspga, IObuff); + ga_concat_len(&lspga, ga.ga_data, ga.ga_len); + ga_clear(&ga); + return lspga.ga_data; + } #endif static void *** ../vim-8.2.4647/src/proto/json.pro 2019-12-12 11:55:25.000000000 +0000 --- src/proto/json.pro 2022-03-30 10:03:09.022655971 +0100 *************** *** 1,6 **** --- 1,7 ---- /* json.c */ char_u *json_encode(typval_T *val, int options); char_u *json_encode_nr_expr(int nr, typval_T *val, int options); + char_u *json_encode_lsp_msg(typval_T *val); int json_decode(js_read_T *reader, typval_T *res, int options); int json_find_end(js_read_T *reader, int options); void f_js_decode(typval_T *argvars, typval_T *rettv); *** ../vim-8.2.4647/src/structs.h 2022-03-29 11:38:13.643070603 +0100 --- src/structs.h 2022-03-30 10:03:09.026656008 +0100 *************** *** 2193,2198 **** --- 2193,2199 ---- MODE_RAW, MODE_JSON, MODE_JS, + MODE_LSP // Language Server Protocol (http + json) } ch_mode_T; typedef enum { *** ../vim-8.2.4647/src/testdir/test_channel.vim 2021-12-17 11:44:29.640404079 +0000 --- src/testdir/test_channel.vim 2022-03-30 10:03:09.026656008 +0100 *************** *** 2378,2382 **** --- 2378,2591 ---- call assert_fails('call job_start([0zff])', 'E976:') endfunc + " Test for the 'lsp' channel mode + func LspCb(chan, msg) + call add(g:lspNotif, a:msg) + endfunc + + func LspOtCb(chan, msg) + call add(g:lspOtMsgs, a:msg) + endfunc + + func LspTests(port) + " call ch_logfile('Xlsprpc.log', 'w') + let ch = ch_open(s:localhost .. a:port, #{mode: 'lsp', callback: 'LspCb'}) + if ch_status(ch) == "fail" + call assert_report("Can't open the lsp channel") + return + endif + + " check for channel information + let info = ch_info(ch) + call assert_equal('LSP', info.sock_mode) + + " Evaluate an expression + let resp = ch_evalexpr(ch, #{method: 'simple-rpc', params: [10, 20]}) + call assert_false(empty(resp)) + call assert_equal(#{id: 1, jsonrpc: '2.0', result: 'simple-rpc'}, resp) + + " Evaluate an expression. While waiting for the response, a notification + " message is delivered. + let g:lspNotif = [] + let resp = ch_evalexpr(ch, #{method: 'rpc-with-notif', params: {'v': 10}}) + call assert_false(empty(resp)) + call assert_equal(#{id: 2, jsonrpc: '2.0', result: 'rpc-with-notif-resp'}, + \ resp) + call assert_equal([#{jsonrpc: '2.0', result: 'rpc-with-notif-notif'}], + \ g:lspNotif) + + " Wrong payload notification test + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'wrong-payload', params: {}}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', result: 'wrong-payload'}], g:lspNotif) + + " Test for receiving a response with incorrect 'id' and additional + " notification messages while evaluating an expression. + let g:lspNotif = [] + let resp = ch_evalexpr(ch, #{method: 'rpc-resp-incorrect-id', + \ params: {'a': [1, 2]}}) + call assert_false(empty(resp)) + call assert_equal(#{id: 4, jsonrpc: '2.0', + \ result: 'rpc-resp-incorrect-id-4'}, resp) + call assert_equal([#{jsonrpc: '2.0', result: 'rpc-resp-incorrect-id-1'}, + \ #{jsonrpc: '2.0', result: 'rpc-resp-incorrect-id-2'}, + \ #{jsonrpc: '2.0', id: 1, result: 'rpc-resp-incorrect-id-3'}], + \ g:lspNotif) + + " simple notification test + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'simple-notif', params: [#{a: 10, b: []}]}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', result: 'simple-notif'}], g:lspNotif) + + " multiple notifications test + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'multi-notif', params: [#{a: {}, b: {}}]}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', result: 'multi-notif1'}, + \ #{jsonrpc: '2.0', result: 'multi-notif2'}], g:lspNotif) + + " Test for sending a message with an identifier. + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'msg-with-id', id: 93, params: #{s: 'str'}}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', id: 93, result: 'msg-with-id'}], + \ g:lspNotif) + + " Test for setting the 'id' value in a request message + let resp = ch_evalexpr(ch, #{method: 'ping', id: 1, params: {}}) + call assert_equal(#{id: 8, jsonrpc: '2.0', result: 'alive'}, resp) + + " Test for using a one time callback function to process a response + let g:lspOtMsgs = [] + call ch_sendexpr(ch, #{method: 'msg-specifc-cb', params: {}}, + \ #{callback: 'LspOtCb'}) + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{id: 9, jsonrpc: '2.0', result: 'msg-specifc-cb'}], + \ g:lspOtMsgs) + + " Test for generating a request message from the other end (server) + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'server-req', params: #{}}) + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([{'id': 201, 'jsonrpc': '2.0', + \ 'result': {'method': 'checkhealth', 'params': {'a': 20}}}], + \ g:lspNotif) + + " Test for sending a message without an id + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'echo', params: #{s: 'msg-without-id'}}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', result: + \ #{method: 'echo', jsonrpc: '2.0', params: #{s: 'msg-without-id'}}}], + \ g:lspNotif) + + " Test for sending a notification message with an id + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'echo', id: 110, params: #{s: 'msg-with-id'}}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', result: + \ #{method: 'echo', jsonrpc: '2.0', id: 110, + \ params: #{s: 'msg-with-id'}}}], g:lspNotif) + + " Test for processing the extra fields in the HTTP header + let resp = ch_evalexpr(ch, #{method: 'extra-hdr-fields', params: {}}) + call assert_equal({'id': 14, 'jsonrpc': '2.0', 'result': 'extra-hdr-fields'}, + \ resp) + + " Test for processing a HTTP header without the Content-Length field + let resp = ch_evalexpr(ch, #{method: 'hdr-without-len', params: {}}, + \ #{timeout: 200}) + call assert_equal('', resp) + " send a ping to make sure communication still works + let resp = ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal({'id': 16, 'jsonrpc': '2.0', 'result': 'alive'}, resp) + + " Test for processing a HTTP header with wrong length + let resp = ch_evalexpr(ch, #{method: 'hdr-with-wrong-len', params: {}}, + \ #{timeout: 200}) + call assert_equal('', resp) + " send a ping to make sure communication still works + let resp = ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal({'id': 18, 'jsonrpc': '2.0', 'result': 'alive'}, resp) + + " Test for processing a HTTP header with negative length + let resp = ch_evalexpr(ch, #{method: 'hdr-with-negative-len', params: {}}, + \ #{timeout: 200}) + call assert_equal('', resp) + " send a ping to make sure communication still works + let resp = ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal({'id': 20, 'jsonrpc': '2.0', 'result': 'alive'}, resp) + + " Test for an empty header + let resp = ch_evalexpr(ch, #{method: 'empty-header', params: {}}, + \ #{timeout: 200}) + call assert_equal('', resp) + " send a ping to make sure communication still works + let resp = ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal({'id': 22, 'jsonrpc': '2.0', 'result': 'alive'}, resp) + + " Test for an empty payload + let resp = ch_evalexpr(ch, #{method: 'empty-payload', params: {}}, + \ #{timeout: 200}) + call assert_equal('', resp) + " send a ping to make sure communication still works + let resp = ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal({'id': 24, 'jsonrpc': '2.0', 'result': 'alive'}, resp) + + " Test for invoking an unsupported method + let resp = ch_evalexpr(ch, #{method: 'xyz', params: {}}, #{timeout: 200}) + call assert_equal('', resp) + + " Test for sending a message without a callback function. Notification + " message should be dropped but RPC response should not be dropped. + call ch_setoptions(ch, #{callback: ''}) + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'echo', params: #{s: 'no-callback'}}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([], g:lspNotif) + " Restore the callback function + call ch_setoptions(ch, #{callback: 'LspCb'}) + let g:lspNotif = [] + call ch_sendexpr(ch, #{method: 'echo', params: #{s: 'no-callback'}}) + " Send a ping to wait for all the notification messages to arrive + call ch_evalexpr(ch, #{method: 'ping'}) + call assert_equal([#{jsonrpc: '2.0', result: + \ #{method: 'echo', jsonrpc: '2.0', params: #{s: 'no-callback'}}}], + \ g:lspNotif) + + " " Test for sending a raw message + " let g:lspNotif = [] + " let s = "Content-Length: 62\r\n" + " let s ..= "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n" + " let s ..= "\r\n" + " let s ..= '{"method":"echo","jsonrpc":"2.0","params":{"m":"raw-message"}}' + " call ch_sendraw(ch, s) + " call ch_evalexpr(ch, #{method: 'ping'}) + " call assert_equal([{'jsonrpc': '2.0', + " \ 'result': {'method': 'echo', 'jsonrpc': '2.0', + " \ 'params': {'m': 'raw-message'}}}], 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_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:') + call assert_fails('call ch_evalexpr(ch, #{method: "ping"}, #{callback: "LspOtCb"})', 'E917:') + " call ch_logfile('', 'w') + endfunc + + func Test_channel_lsp_mode() + call RunServer('test_channel_lsp.py', 'LspTests', []) + endfunc " vim: shiftwidth=2 sts=2 expandtab *** ../vim-8.2.4647/src/testdir/test_channel_lsp.py 2022-03-30 10:14:06.397625577 +0100 --- src/testdir/test_channel_lsp.py 2022-03-30 10:03:09.026656008 +0100 *************** *** 0 **** --- 1,299 ---- + #!/usr/bin/env python + # + # Server that will accept connections from a Vim channel. + # Used by test_channel.vim to test LSP functionality. + # + # This requires Python 2.6 or later. + + from __future__ import print_function + import json + import socket + import sys + import time + import threading + + try: + # Python 3 + import socketserver + except ImportError: + # Python 2 + import SocketServer as socketserver + + class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): + + def setup(self): + self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + def send_lsp_msg(self, msgid, resp_dict): + v = {'jsonrpc': '2.0', 'result': resp_dict} + if msgid != -1: + v['id'] = msgid + s = json.dumps(v) + resp = "Content-Length: " + str(len(s)) + "\r\n" + resp += "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n" + resp += "\r\n" + resp += s + if self.debug: + with open("Xlspdebug.log", "a") as myfile: + myfile.write("\n=> send\n" + resp) + self.request.sendall(resp.encode('utf-8')) + + def send_wrong_payload(self): + v = 'wrong-payload' + s = json.dumps(v) + resp = "Content-Length: " + str(len(s)) + "\r\n" + resp += "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n" + resp += "\r\n" + resp += s + self.request.sendall(resp.encode('utf-8')) + + def send_empty_header(self, msgid, resp_dict): + v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict} + s = json.dumps(v) + resp = "\r\n" + resp += s + self.request.sendall(resp.encode('utf-8')) + + def send_empty_payload(self): + resp = "Content-Length: 0\r\n" + resp += "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n" + resp += "\r\n" + self.request.sendall(resp.encode('utf-8')) + + def send_extra_hdr_fields(self, msgid, resp_dict): + # test for sending extra fields in the http header + v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict} + s = json.dumps(v) + resp = "Host: abc.vim.org\r\n" + resp += "User-Agent: Python\r\n" + resp += "Accept-Language: en-US,en\r\n" + resp += "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n" + resp += "Content-Length: " + str(len(s)) + "\r\n" + resp += "\r\n" + resp += s + self.request.sendall(resp.encode('utf-8')) + + def send_hdr_without_len(self, msgid, resp_dict): + # test for sending the http header without length + v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict} + s = json.dumps(v) + resp = "Content-Type: application/vim-jsonrpc; charset=utf-8\r\n" + resp += "\r\n" + resp += s + self.request.sendall(resp.encode('utf-8')) + + def send_hdr_with_wrong_len(self, msgid, resp_dict): + # test for sending the http header with wrong length + v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict} + s = json.dumps(v) + resp = "Content-Length: 1000\r\n" + resp += "\r\n" + resp += s + self.request.sendall(resp.encode('utf-8')) + + def send_hdr_with_negative_len(self, msgid, resp_dict): + # test for sending the http header with negative length + v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict} + s = json.dumps(v) + resp = "Content-Length: -1\r\n" + resp += "\r\n" + resp += s + self.request.sendall(resp.encode('utf-8')) + + def do_ping(self, payload): + time.sleep(0.2) + self.send_lsp_msg(payload['id'], 'alive') + + def do_echo(self, payload): + self.send_lsp_msg(-1, payload) + + def do_simple_rpc(self, payload): + # test for a simple RPC request + self.send_lsp_msg(payload['id'], 'simple-rpc') + + def do_rpc_with_notif(self, payload): + # test for sending a notification before replying to a request message + self.send_lsp_msg(-1, 'rpc-with-notif-notif') + # sleep for some time to make sure the notification is delivered + time.sleep(0.2) + self.send_lsp_msg(payload['id'], 'rpc-with-notif-resp') + + def do_wrong_payload(self, payload): + # test for sending a non dict payload + self.send_wrong_payload() + time.sleep(0.2) + self.send_lsp_msg(-1, 'wrong-payload') + + def do_rpc_resp_incorrect_id(self, payload): + self.send_lsp_msg(-1, 'rpc-resp-incorrect-id-1') + self.send_lsp_msg(-1, 'rpc-resp-incorrect-id-2') + self.send_lsp_msg(1, 'rpc-resp-incorrect-id-3') + time.sleep(0.2) + self.send_lsp_msg(payload['id'], 'rpc-resp-incorrect-id-4') + + def do_simple_notif(self, payload): + # notification message test + self.send_lsp_msg(-1, 'simple-notif') + + def do_multi_notif(self, payload): + # send multiple notifications + self.send_lsp_msg(-1, 'multi-notif1') + self.send_lsp_msg(-1, 'multi-notif2') + + def do_msg_with_id(self, payload): + self.send_lsp_msg(payload['id'], 'msg-with-id') + + def do_msg_specific_cb(self, payload): + self.send_lsp_msg(payload['id'], 'msg-specifc-cb') + + def do_server_req(self, payload): + self.send_lsp_msg(201, {'method': 'checkhealth', 'params': {'a': 20}}) + + def do_extra_hdr_fields(self, payload): + self.send_extra_hdr_fields(payload['id'], 'extra-hdr-fields') + + def do_hdr_without_len(self, payload): + self.send_hdr_without_len(payload['id'], 'hdr-without-len') + + def do_hdr_with_wrong_len(self, payload): + self.send_hdr_with_wrong_len(payload['id'], 'hdr-with-wrong-len') + + def do_hdr_with_negative_len(self, payload): + self.send_hdr_with_negative_len(payload['id'], 'hdr-with-negative-len') + + def do_empty_header(self, payload): + self.send_empty_header(payload['id'], 'empty-header') + + def do_empty_payload(self, payload): + self.send_empty_payload() + + def process_msg(self, msg): + try: + decoded = json.loads(msg) + print("Decoded:") + print(str(decoded)) + if 'method' in decoded: + test_map = { + 'ping': self.do_ping, + 'echo': self.do_echo, + 'simple-rpc': self.do_simple_rpc, + 'rpc-with-notif': self.do_rpc_with_notif, + 'wrong-payload': self.do_wrong_payload, + 'rpc-resp-incorrect-id': self.do_rpc_resp_incorrect_id, + 'simple-notif': self.do_simple_notif, + 'multi-notif': self.do_multi_notif, + 'msg-with-id': self.do_msg_with_id, + 'msg-specifc-cb': self.do_msg_specific_cb, + 'server-req': self.do_server_req, + 'extra-hdr-fields': self.do_extra_hdr_fields, + 'hdr-without-len': self.do_hdr_without_len, + 'hdr-with-wrong-len': self.do_hdr_with_wrong_len, + 'hdr-with-negative-len': self.do_hdr_with_negative_len, + 'empty-header': self.do_empty_header, + 'empty-payload': self.do_empty_payload + } + if decoded['method'] in test_map: + test_map[decoded['method']](decoded) + else: + print("Error: Unsupported method: " + decoded['method']) + else: + print("Error: 'method' field is not found") + + except ValueError: + print("json decoding failed") + + def process_msgs(self, msgbuf): + while True: + sidx = msgbuf.find('Content-Length: ') + if sidx == -1: + return msgbuf + sidx += 16 + eidx = msgbuf.find('\r\n') + if eidx == -1: + return msgbuf + msglen = int(msgbuf[sidx:eidx]) + + hdrend = msgbuf.find('\r\n\r\n') + if hdrend == -1: + return msgbuf + + # Remove the header + msgbuf = msgbuf[hdrend + 4:] + payload = msgbuf[:msglen] + + self.process_msg(payload) + + # Remove the processed message + msgbuf = msgbuf[msglen:] + + def handle(self): + print("=== socket opened ===") + self.debug = False + msgbuf = '' + while True: + try: + received = self.request.recv(4096).decode('utf-8') + except socket.error: + print("=== socket error ===") + break + except IOError: + print("=== socket closed ===") + break + if received == '': + print("=== socket closed ===") + break + print("\nReceived:\n{0}".format(received)) + + # Write the received lines into the file for debugging + if self.debug: + with open("Xlspdebug.log", "a") as myfile: + myfile.write("\n<= recv\n" + received) + + # Can receive more than one line in a response or a partial line. + # Accumulate all the received characters and process one line at + # a time. + msgbuf += received + msgbuf = self.process_msgs(msgbuf) + + class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + pass + + def writePortInFile(port): + # Write the port number in Xportnr, so that the test knows it. + f = open("Xportnr", "w") + f.write("{0}".format(port)) + f.close() + + def main(host, port, server_class=ThreadedTCPServer): + # Wait half a second before opening the port to test waittime in ch_open(). + # We do want to get the port number, get that first. We cannot open the + # socket, guess a port is free. + if len(sys.argv) >= 2 and sys.argv[1] == 'delay': + port = 13684 + writePortInFile(port) + + print("Wait for it...") + time.sleep(0.5) + + server = server_class((host, port), ThreadedTCPRequestHandler) + ip, port = server.server_address[0:2] + + # Start a thread with the server. That thread will then start a new thread + # for each connection. + server_thread = threading.Thread(target=server.serve_forever) + server_thread.start() + + writePortInFile(port) + + print("Listening on port {0}".format(port)) + + # Main thread terminates, but the server continues running + # until server.shutdown() is called. + try: + while server_thread.is_alive(): + server_thread.join(1) + except (KeyboardInterrupt, SystemExit): + server.shutdown() + + if __name__ == "__main__": + main("localhost", 0) *** ../vim-8.2.4647/src/version.c 2022-03-29 19:52:08.787653549 +0100 --- src/version.c 2022-03-30 10:07:46.028616256 +0100 *************** *** 752,753 **** --- 752,755 ---- { /* Add new patch number below this line */ + /**/ + 4648, /**/ -- ARTHUR: Listen, old crone! Unless you tell us where we can buy a shrubbery, my friend and I will ... we will say "Ni!" CRONE: Do your worst! "Monty Python and the Holy Grail" PYTHON (MONTY) PICTURES LTD /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\\ /// \\\ \\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ /// \\\ help me help AIDS victims -- http://ICCF-Holland.org ///