]> granicus.if.org Git - vim/commitdiff
patch 8.2.4780: parsing an LSP message fails when it is split v8.2.4780
authorYegappan Lakshmanan <yegappan@yahoo.com>
Mon, 18 Apr 2022 13:07:46 +0000 (14:07 +0100)
committerBram Moolenaar <Bram@vim.org>
Mon, 18 Apr 2022 13:07:46 +0000 (14:07 +0100)
Problem:    Parsing an LSP message fails when it is split.
Solution:   Collapse the received data before parsing. (Yegappan Lakshmanan,
            closes #10215)

runtime/doc/channel.txt
src/channel.c
src/testdir/test_channel.vim
src/testdir/test_channel_lsp.py
src/version.c

index ae90430509a1e3d3c5011cd79e0396b554ca4179..43862d8a2a47909058571767e19f5ccfa634cd8b 100644 (file)
@@ -1433,11 +1433,17 @@ To open a channel using the 'lsp' mode with a job, set the 'in_mode' and
     let opts = {}
     let opts.in_mode = 'lsp'
     let opts.out_mode = 'lsp'
+    let opts.err_mode = 'nl'
     let opts.out_cb = function('LspOutCallback')
     let opts.err_cb = function('LspErrCallback')
     let opts.exit_cb = function('LspExitCallback')
     let job = job_start(cmd, opts)
 
+Note that if a job outputs LSP messages on stdout and non-LSP messages on
+stderr, then the channel-callback function should handle both the message
+formats appropriately or you should use a separate callback function for
+"out_cb" and "err_cb" to handle them as shown above.
+
 To synchronously send a JSON-RPC request to the server, use the
 |ch_evalexpr()| function. This function will wait and return the decoded
 response message from the server. You can use either the |channel-timeout| or
index 79f4dbc29b0e74152d37f840c8f1cd5cc3808de7..b981af4006ae53e5e84dded5487a97670a978847 100644 (file)
@@ -2035,22 +2035,24 @@ channel_consume(channel_T *channel, ch_part_T part, int len)
     int
 channel_collapse(channel_T *channel, ch_part_T part, int want_nl)
 {
-    readq_T *head = &channel->ch_part[part].ch_head;
-    readq_T *node = head->rq_next;
-    readq_T *last_node;
-    readq_T *n;
-    char_u  *newbuf;
-    char_u  *p;
-    long_u len;
+    ch_mode_T  mode = channel->ch_part[part].ch_mode;
+    readq_T    *head = &channel->ch_part[part].ch_head;
+    readq_T    *node = head->rq_next;
+    readq_T    *last_node;
+    readq_T    *n;
+    char_u     *newbuf;
+    char_u     *p;
+    long_u     len;
 
     if (node == NULL || node->rq_next == NULL)
        return FAIL;
 
     last_node = node->rq_next;
     len = node->rq_buflen + last_node->rq_buflen;
-    if (want_nl)
+    if (want_nl || mode == MODE_LSP)
        while (last_node->rq_next != NULL
-               && channel_first_nl(last_node) == NULL)
+               && (mode == MODE_LSP
+                   || channel_first_nl(last_node) == NULL))
        {
            last_node = last_node->rq_next;
            len += last_node->rq_buflen;
@@ -3006,6 +3008,12 @@ 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 == 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.
+               (void)channel_collapse(channel, part, FALSE);
+
            // Parse readahead, return when there is still no message.
            channel_parse_json(channel, part);
            if (channel_get_json(channel, part, -1, FALSE, &listtv) == FAIL)
@@ -3974,6 +3982,7 @@ channel_read_json_block(
     sock_T     fd;
     int                timeout;
     chanpart_T *chanpart = &channel->ch_part[part];
+    ch_mode_T  mode = channel->ch_part[part].ch_mode;
     int                retval = FAIL;
 
     ch_log(channel, "Blocking read JSON for id %d", id);
@@ -3984,6 +3993,12 @@ channel_read_json_block(
 
     for (;;)
     {
+       if (mode == 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.
+           (void)channel_collapse(channel, part, FALSE);
+
        more = channel_parse_json(channel, part);
 
        // search for message "id"
index e270a4be7a120ba8279c57ef704b346c8a740288..6fef421e56aae5370dd5c07a10975d579ad729f0 100644 (file)
@@ -2580,6 +2580,11 @@ func LspTests(port)
   call assert_equal({'id': 14, 'jsonrpc': '2.0', 'result': 'extra-hdr-fields'},
         \ resp)
 
+  " Test for processing delayed payload
+  let resp = ch_evalexpr(ch, #{method: 'delayed-payload', params: {}})
+  call assert_equal({'id': 15, 'jsonrpc': '2.0', 'result': 'delayed-payload'},
+        \ resp)
+
   " Test for processing a HTTP header without the Content-Length field
   let resp = ch_evalexpr(ch, #{method: 'hdr-without-len', params: {}},
         \ #{timeout: 200})
@@ -2629,13 +2634,6 @@ func LspTests(port)
   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 assert_equal('alive', ch_evalexpr(ch, #{method: 'ping'}).result)
-  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 = []
index 530258d844006e9e882f02753061b8ff64b97a39..fb8ed2243f3915c9eec47becc8c3a63df15109bb 100644 (file)
@@ -73,6 +73,18 @@ class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
         resp += s
         self.request.sendall(resp.encode('utf-8'))
 
+    def send_delayed_payload(self, msgid, resp_dict):
+        # test for sending the hdr first and then after some delay, send the
+        # payload
+        v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict}
+        s = json.dumps(v)
+        resp = "Content-Length: " + str(len(s)) + "\r\n"
+        resp += "\r\n"
+        self.request.sendall(resp.encode('utf-8'))
+        time.sleep(0.05)
+        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}
@@ -152,6 +164,9 @@ class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
     def do_extra_hdr_fields(self, payload):
         self.send_extra_hdr_fields(payload['id'], 'extra-hdr-fields')
 
+    def do_delayad_payload(self, payload):
+        self.send_delayed_payload(payload['id'], 'delayed-payload')
+
     def do_hdr_without_len(self, payload):
         self.send_hdr_without_len(payload['id'], 'hdr-without-len')
 
@@ -186,6 +201,7 @@ class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
                         'msg-specifc-cb': self.do_msg_specific_cb,
                         'server-req': self.do_server_req,
                         'extra-hdr-fields': self.do_extra_hdr_fields,
+                        'delayed-payload': self.do_delayad_payload,
                         '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,
index efc01043223b5e5fe01862c7092f405437ffc1e9..fd461229f88173255789145830448fe1689b7cfb 100644 (file)
@@ -746,6 +746,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    4780,
 /**/
     4779,
 /**/