From 28c6ec9cad3064966ff70c9da95c96364118eb57 Mon Sep 17 00:00:00 2001 From: w0rp Date: Mon, 8 May 2017 22:18:28 +0100 Subject: [PATCH] #517 - Implement LSP chunked message parsing, sending messages to sockets, and callbacks --- autoload/ale/lsp.vim | 175 ++++++++++------ autoload/ale/lsp/message.vim | 43 ++-- autoload/ale/lsp/response.vim | 44 ++++ test/lsp/test_lsp_client_messages.vader | 78 +++++++ test/lsp/test_lsp_connections.vader | 111 ++++++++++ test/lsp/test_read_lsp_diagnostics.vader | 121 +++++++++++ test/test_lsp_client_messages.vader | 254 ----------------------- 7 files changed, 492 insertions(+), 334 deletions(-) create mode 100644 autoload/ale/lsp/response.vim create mode 100644 test/lsp/test_lsp_client_messages.vader create mode 100644 test/lsp/test_lsp_connections.vader create mode 100644 test/lsp/test_read_lsp_diagnostics.vader delete mode 100644 test/test_lsp_client_messages.vader diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index e01e4eb6..72b94427 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -1,6 +1,7 @@ " Author: w0rp " Description: Language Server Protocol client code +let s:address_info_map = {} let g:ale_lsp_next_message_id = 1 function! ale#lsp#GetNextMessageID() abort @@ -19,75 +20,133 @@ function! ale#lsp#GetNextMessageID() abort return l:id endfunction -" (method_name, params) -function! ale#lsp#CreateMessage(method_name, ...) abort +" Given a List of one or two items, [method_name] or [method_name, params], +" return a List containing [message_id, message_data] +function! ale#lsp#CreateMessageData(message) abort + let l:is_notification = a:message[0] + + let l:obj = { + \ 'id': v:null, + \ 'jsonrpc': '2.0', + \ 'method': a:message[1], + \} + + if !l:is_notification + let l:obj.id = ale#lsp#GetNextMessageID() + endif + + if len(a:message) > 2 + let l:obj.params = a:message[2] + endif + + let l:body = json_encode(l:obj) + let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body + + return [l:is_notification ? 0 : l:obj.id, l:data] +endfunction + +function! ale#lsp#ReadMessageData(data) abort + let l:response_list = [] + let l:remainder = a:data + + while 1 + " Look for the end of the HTTP headers + let l:body_start_index = matchend(l:remainder, "\r\n\r\n") + + if l:body_start_index < 0 + " No header end was found yet. + break + endif + + " Parse the Content-Length header. + let l:header_data = l:remainder[:l:body_start_index - 4] + let l:length_match = matchlist( + \ l:header_data, + \ '\vContent-Length: *(\d+)' + \) + + if empty(l:length_match) + throw "Invalid JSON-RPC header:\n" . l:header_data + endif + + " Split the body and the remainder of the text. + let l:remainder_start_index = l:body_start_index + str2nr(l:length_match[1]) + + if len(l:remainder) < l:remainder_start_index + " We don't have enough data yet. + break + endif + + let l:body = l:remainder[l:body_start_index : l:remainder_start_index - 1] + let l:remainder = l:remainder[l:remainder_start_index :] + + " Parse the JSON object and add it to the list. + call add(l:response_list, json_decode(l:body)) + endwhile + + return [l:remainder, l:response_list] +endfunction + +function! s:HandleMessage(channel, message) abort + let l:channel_info = ch_info(a:channel) + let l:address = l:channel_info.hostname . ':' . l:channel_info.port + let l:info = s:address_info_map[l:address] + let l:info.data .= a:message + + " Parse the objects now if we can, and keep the remaining text. + let [l:info.data, l:response_list] = ale#lsp#ReadMessageData(l:info.data) + + " Call our callbacks. + for l:response in l:response_list + let l:callback = l:info.callback_map.pop(l:response.id) + call ale#util#GetFunction(l:callback)(l:response) + endfor +endfunction + +" Send a message to the server. +" A callback can be registered to handle the response. +" Notifications do not need to be handled. +" (address, message, callback?) +function! ale#lsp#SendMessage(address, message, ...) abort if a:0 > 1 throw 'Too many arguments!' endif - let l:obj = { - \ 'id': ale#lsp#GetNextMessageID(), - \ 'jsonrpc': '2.0', - \ 'method': a:method_name, - \} - - if a:0 > 0 - let l:obj.params = a:1 + if !a:message[0] && a:0 == 0 + throw 'A callback must be set for messages which are not notifications!' endif - let l:body = json_encode(l:obj) + let [l:id, l:data] = ale#lsp#CreateMessageData(a:message) - return 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body -endfunction + let l:info = get(s:address_info_map, a:address, {}) -function! ale#lsp#ReadMessage(data) abort - let l:header_end_index = match(a:data, "\r\n\r\n") - - if l:header_end_index < 0 - throw 'Invalid messaage: ' . string(a:data) - endif - - return json_decode(a:data[l:header_end_index + 4:]) -endfunction - -" Constants for message severity codes. -let s:SEVERITY_ERROR = 1 -let s:SEVERITY_WARNING = 2 -let s:SEVERITY_INFORMATION = 3 -let s:SEVERITY_HINT = 4 - -" Parse the message for textDocument/publishDiagnostics -function! ale#lsp#ReadDiagnostics(params) abort - let l:filename = a:params.uri - let l:loclist = [] - - for l:diagnostic in a:params.diagnostics - let l:severity = get(l:diagnostic, 'severity', 0) - let l:loclist_item = { - \ 'message': l:diagnostic.message, - \ 'type': 'E', - \ 'lnum': l:diagnostic.range.start.line + 1, - \ 'col': l:diagnostic.range.start.character + 1, - \ 'end_lnum': l:diagnostic.range.end.line + 1, - \ 'end_col': l:diagnostic.range.end.character + 1, + if empty(l:info) + let l:info = { + \ 'data': '', + \ 'callback_map': {}, \} + let s:address_info_map[a:address] = l:info + endif - if l:severity == s:SEVERITY_WARNING - let l:loclist_item.type = 'W' - elseif l:severity == s:SEVERITY_INFORMATION - " TODO: Use 'I' here in future. - let l:loclist_item.type = 'W' - elseif l:severity == s:SEVERITY_HINT - " TODO: Use 'H' here in future - let l:loclist_item.type = 'W' - endif + " The ID is 0 when the message is a Notification, which is a JSON-RPC + " request for which the server must not return a response. + if l:id != 0 + " Add the callback, which the server will respond to later. + let l:info.callback_map[l:id] = a:1 + endif - if has_key(l:diagnostic, 'code') - let l:loclist_item.nr = l:diagnostic.code - endif + if !has_key(l:info, 'channel') || ch_status(l:info.channel) !=# 'open' + let l:info.channnel = ch_open(a:address, { + \ 'mode': 'raw', + \ 'waittime': 0, + \ 'callback': 's:HandleMessage', + \}) + endif - call add(l:loclist, l:loclist_item) - endfor + if ch_status(l:info.channnel) ==# 'fail' + throw 'Failed to open channel for: ' . a:address + endif - return [l:filename, l:loclist] + " Send the message to the server + call ch_sendraw(l:info.channel, l:data) endfunction diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index d46e68ab..937e4f46 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -1,65 +1,64 @@ " Author: w0rp " Description: Language Server Protocol message implementations +" +" Messages in this movie will be returned in the format +" [is_notification, method_name, params?] -function! ale#lsp#message#CancelRequest(id) abort - return ale#lsp#CreateMessage('$/cancelRequest', {'id': a:id}) -endfunction - -function! ale#lsp#message#Initialize(processId, rootUri) abort +function! ale#lsp#message#Initialize(root_uri) abort " TODO: Define needed capabilities. - return ale#lsp#CreateMessage('initialize', { - \ 'processId': a:processId, - \ 'rootUri': a:rootUri, + return [0, 'initialize', { + \ 'processId': getpid(), + \ 'rootUri': a:root_uri, \ 'capabilities': {}, - \}) + \}] endfunction function! ale#lsp#message#Initialized() abort - return ale#lsp#CreateMessage('initialized') + return [1, 'initialized'] endfunction function! ale#lsp#message#Shutdown() abort - return ale#lsp#CreateMessage('shutdown') + return [0, 'shutdown'] endfunction function! ale#lsp#message#Exit() abort - return ale#lsp#CreateMessage('exit') + return [1, 'exit'] endfunction -function! ale#lsp#message#DidOpen(uri, languageId, version, text) abort - return ale#lsp#CreateMessage('textDocument/didOpen', { +function! ale#lsp#message#DidOpen(uri, language_id, version, text) abort + return [1, 'textDocument/didOpen', { \ 'textDocument': { \ 'uri': a:uri, - \ 'languageId': a:languageId, + \ 'languageId': a:language_id, \ 'version': a:version, \ 'text': a:text, \ }, - \}) + \}] endfunction function! ale#lsp#message#DidChange(uri, version, text) abort " For changes, we simply send the full text of the document to the server. - return ale#lsp#CreateMessage('textDocument/didChange', { + return [1, 'textDocument/didChange', { \ 'textDocument': { \ 'uri': a:uri, \ 'version': a:version, \ }, \ 'contentChanges': [{'text': a:text}] - \}) + \}] endfunction function! ale#lsp#message#DidSave(uri) abort - return ale#lsp#CreateMessage('textDocument/didSave', { + return [1, 'textDocument/didSave', { \ 'textDocument': { \ 'uri': a:uri, \ }, - \}) + \}] endfunction function! ale#lsp#message#DidClose(uri) abort - return ale#lsp#CreateMessage('textDocument/didClose', { + return [1, 'textDocument/didClose', { \ 'textDocument': { \ 'uri': a:uri, \ }, - \}) + \}] endfunction diff --git a/autoload/ale/lsp/response.vim b/autoload/ale/lsp/response.vim new file mode 100644 index 00000000..aeb93a58 --- /dev/null +++ b/autoload/ale/lsp/response.vim @@ -0,0 +1,44 @@ +" Author: w0rp +" Description: Parsing and transforming of LSP server responses. + +" Constants for message severity codes. +let s:SEVERITY_ERROR = 1 +let s:SEVERITY_WARNING = 2 +let s:SEVERITY_INFORMATION = 3 +let s:SEVERITY_HINT = 4 + +" Parse the message for textDocument/publishDiagnostics +function! ale#lsp#response#ReadDiagnostics(params) abort + let l:filename = a:params.uri + let l:loclist = [] + + for l:diagnostic in a:params.diagnostics + let l:severity = get(l:diagnostic, 'severity', 0) + let l:loclist_item = { + \ 'message': l:diagnostic.message, + \ 'type': 'E', + \ 'lnum': l:diagnostic.range.start.line + 1, + \ 'col': l:diagnostic.range.start.character + 1, + \ 'end_lnum': l:diagnostic.range.end.line + 1, + \ 'end_col': l:diagnostic.range.end.character + 1, + \} + + if l:severity == s:SEVERITY_WARNING + let l:loclist_item.type = 'W' + elseif l:severity == s:SEVERITY_INFORMATION + " TODO: Use 'I' here in future. + let l:loclist_item.type = 'W' + elseif l:severity == s:SEVERITY_HINT + " TODO: Use 'H' here in future + let l:loclist_item.type = 'W' + endif + + if has_key(l:diagnostic, 'code') + let l:loclist_item.nr = l:diagnostic.code + endif + + call add(l:loclist, l:loclist_item) + endfor + + return [l:filename, l:loclist] +endfunction diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader new file mode 100644 index 00000000..de18a4b8 --- /dev/null +++ b/test/lsp/test_lsp_client_messages.vader @@ -0,0 +1,78 @@ +Execute(ale#lsp#message#Initialize() should return correct messages): + AssertEqual + \ [ + \ 0, + \ 'initialize', + \ { + \ 'processId': getpid(), + \ 'rootUri': '/foo/bar', + \ 'capabilities': {}, + \ } + \ ], + \ ale#lsp#message#Initialize('/foo/bar') + +Execute(ale#lsp#message#Initialized() should return correct messages): + AssertEqual [1, 'initialized'], ale#lsp#message#Initialized() + +Execute(ale#lsp#message#Shutdown() should return correct messages): + AssertEqual [0, 'shutdown'], ale#lsp#message#Shutdown() + +Execute(ale#lsp#message#Exit() should return correct messages): + AssertEqual [1, 'exit'], ale#lsp#message#Exit(), + +Execute(ale#lsp#message#DidOpen() should return correct messages): + AssertEqual + \ [ + \ 1, + \ 'textDocument/didOpen', + \ { + \ 'textDocument': { + \ 'uri': '/foo/bar', + \ 'languageId': 'typescript', + \ 'version': 123, + \ 'text': 'foobar', + \ }, + \ } + \ ], + \ ale#lsp#message#DidOpen('/foo/bar', 'typescript', 123, 'foobar') + +Execute(ale#lsp#message#DidChange() should return correct messages): + AssertEqual + \ [ + \ 1, + \ 'textDocument/didChange', + \ { + \ 'textDocument': { + \ 'uri': '/foo/bar', + \ 'version': 123, + \ }, + \ 'contentChanges': [{'text': 'foobar'}], + \ } + \ ], + \ ale#lsp#message#DidChange('/foo/bar', 123, 'foobar') + +Execute(ale#lsp#message#DidSave() should return correct messages): + AssertEqual + \ [ + \ 1, + \ 'textDocument/didSave', + \ { + \ 'textDocument': { + \ 'uri': '/foo/bar', + \ }, + \ } + \ ], + \ ale#lsp#message#DidSave('/foo/bar') + +Execute(ale#lsp#message#DidClose() should return correct messages): + AssertEqual + \ [ + \ 1, + \ 'textDocument/didClose', + \ { + \ 'textDocument': { + \ 'uri': '/foo/bar', + \ }, + \ } + \ ], + \ ale#lsp#message#DidClose('/foo/bar') diff --git a/test/lsp/test_lsp_connections.vader b/test/lsp/test_lsp_connections.vader new file mode 100644 index 00000000..36a21bd2 --- /dev/null +++ b/test/lsp/test_lsp_connections.vader @@ -0,0 +1,111 @@ +Before: + let g:ale_lsp_next_message_id = 1 + +After: + unlet! b:data + +Execute(GetNextMessageID() should increment appropriately): + " We should get the initial ID, and increment a bit. + AssertEqual 1, ale#lsp#GetNextMessageID() + AssertEqual 2, ale#lsp#GetNextMessageID() + AssertEqual 3, ale#lsp#GetNextMessageID() + + " Set the maximum ID. + let g:ale_lsp_next_message_id = 9223372036854775807 + + " When we hit the maximum ID, the next ID afterwards should be 1. + AssertEqual 9223372036854775807, ale#lsp#GetNextMessageID() + AssertEqual 1, ale#lsp#GetNextMessageID() + +Execute(ale#lsp#CreateMessageData() should create an appropriate message): + " 71 is the size in bytes for UTF-8, not the number of characters. + AssertEqual + \ [ + \ 1, + \ "Content-Length: 71\r\n\r\n" + \ . '{"id":1,"jsonrpc":"2.0","method":"someMethod","params":{"foo":"barÜ"}}', + \ ], + \ ale#lsp#CreateMessageData([0, 'someMethod', {'foo': 'barÜ'}]) + " Check again to ensure that we use the next ID. + AssertEqual + \ [ + \ 2, + \ "Content-Length: 71\r\n\r\n" + \ . '{"id":2,"jsonrpc":"2.0","method":"someMethod","params":{"foo":"barÜ"}}', + \ ], + \ ale#lsp#CreateMessageData([0, 'someMethod', {'foo': 'barÜ'}]) + +Execute(ale#lsp#CreateMessageData() should create messages without params): + AssertEqual + \ [ + \ 1, + \ "Content-Length: 51\r\n\r\n" + \ . '{"id":1,"jsonrpc":"2.0","method":"someOtherMethod"}', + \ ], + \ ale#lsp#CreateMessageData([0, 'someOtherMethod']) + +Execute(ale#lsp#CreateMessageData() should create notifications): + AssertEqual + \ [ + \ 0, + \ "Content-Length: 55\r\n\r\n" + \ . '{"id":null,"jsonrpc":"2.0","method":"someNotification"}', + \ ], + \ ale#lsp#CreateMessageData([1, 'someNotification']) + AssertEqual + \ [ + \ 0, + \ "Content-Length: 78\r\n\r\n" + \ . '{"id":null,"jsonrpc":"2.0","method":"someNotification","params":{"foo":"bar"}}', + \ ], + \ ale#lsp#CreateMessageData([1, 'someNotification', {'foo': 'bar'}]) + +Execute(ale#lsp#ReadMessageData() should read single whole messages): + AssertEqual + \ ['', [{'id': 2, 'jsonrpc': '2.0', 'result': {'foo': 'barÜ'}}]], + \ ale#lsp#ReadMessageData( + \ "Content-Length: 49\r\n\r\n" + \ . '{"id":2,"jsonrpc":"2.0","result":{"foo":"barÜ"}}' + \ ) + +Execute(ale#lsp#ReadMessageData() should ignore other headers): + AssertEqual + \ ['', [{'id': 2, 'jsonrpc': '2.0', 'result': {'foo': 'barÜ'}}]], + \ ale#lsp#ReadMessageData( + \ "First-Header: 49\r\n" + \ . "Content-Length: 49\r\n" + \ . "Other-Header: 49\r\n" + \ . "\r\n" + \ . '{"id":2,"jsonrpc":"2.0","result":{"foo":"barÜ"}}' + \ ) + +Execute(ale#lsp#ReadMessageData() should handle partial messages): + let b:data = "Content-Length: 49\r\n\r\n" . '{"id":2,"jsonrpc":"2.0","result":' + + AssertEqual [b:data, []], ale#lsp#ReadMessageData(b:data) + +Execute(ale#lsp#ReadMessageData() should handle multiple messages): + AssertEqual + \ ['', [ + \ {'id': 2, 'jsonrpc': '2.0', 'result': {'foo': 'barÜ'}}, + \ {'id': 2, 'jsonrpc': '2.0', 'result': {'foo123': 'barÜ'}}, + \ ]], + \ ale#lsp#ReadMessageData( + \ "Content-Length: 49\r\n\r\n" + \ . '{"id":2,"jsonrpc":"2.0","result":{"foo":"barÜ"}}' + \ . "Content-Length: 52\r\n\r\n" + \ . '{"id":2,"jsonrpc":"2.0","result":{"foo123":"barÜ"}}' + \ ) + +Execute(ale#lsp#ReadMessageData() should handle a message with part of a second message): + let b:data = "Content-Length: 52\r\n\r\n" . '{"id":2,"jsonrpc":"2.' + + AssertEqual + \ [b:data, [ + \ {'id': 2, 'jsonrpc': '2.0', 'result': {'foo': 'barÜ'}}, + \ ]], + \ ale#lsp#ReadMessageData( + \ "Content-Length: 49\r\n\r\n" + \ . '{"id":2,"jsonrpc":"2.0","result":{"foo":"barÜ"}}' + \ . b:data + \ ) diff --git a/test/lsp/test_read_lsp_diagnostics.vader b/test/lsp/test_read_lsp_diagnostics.vader new file mode 100644 index 00000000..b52da1bd --- /dev/null +++ b/test/lsp/test_read_lsp_diagnostics.vader @@ -0,0 +1,121 @@ +Before: + function Range(start_line, start_char, end_line, end_char) abort + return { + \ 'start': {'line': a:start_line, 'character': a:start_char}, + \ 'end': {'line': a:end_line, 'character': a:end_char}, + \} + endfunction + +After: + delfunction Range + +Execute(ale#lsp#response#ReadDiagnostics() should handle errors): + AssertEqual ['filename.ts', [ + \ { + \ 'type': 'E', + \ 'message': 'Something went wrong!', + \ 'lnum': 3, + \ 'col': 11, + \ 'end_lnum': 5, + \ 'end_col': 16, + \ 'nr': 'some-error', + \ } + \ ]], + \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ + \ { + \ 'severity': 1, + \ 'range': Range(2, 10, 4, 15), + \ 'code': 'some-error', + \ 'message': 'Something went wrong!', + \ }, + \ ]}) + +Execute(ale#lsp#response#ReadDiagnostics() should handle warnings): + AssertEqual ['filename.ts', [ + \ { + \ 'type': 'W', + \ 'message': 'Something went wrong!', + \ 'lnum': 2, + \ 'col': 4, + \ 'end_lnum': 2, + \ 'end_col': 4, + \ 'nr': 'some-warning', + \ } + \ ]], + \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ + \ { + \ 'severity': 2, + \ 'range': Range(1, 3, 1, 3), + \ 'code': 'some-warning', + \ 'message': 'Something went wrong!', + \ }, + \ ]}) + +Execute(ale#lsp#response#ReadDiagnostics() should treat messages with missing severity as errors): + AssertEqual ['filename.ts', [ + \ { + \ 'type': 'E', + \ 'message': 'Something went wrong!', + \ 'lnum': 3, + \ 'col': 11, + \ 'end_lnum': 5, + \ 'end_col': 16, + \ 'nr': 'some-error', + \ } + \ ]], + \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ + \ { + \ 'range': Range(2, 10, 4, 15), + \ 'code': 'some-error', + \ 'message': 'Something went wrong!', + \ }, + \ ]}) + +Execute(ale#lsp#response#ReadDiagnostics() should handle messages without codes): + AssertEqual ['filename.ts', [ + \ { + \ 'type': 'E', + \ 'message': 'Something went wrong!', + \ 'lnum': 3, + \ 'col': 11, + \ 'end_lnum': 5, + \ 'end_col': 16, + \ } + \ ]], + \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ + \ { + \ 'range': Range(2, 10, 4, 15), + \ 'message': 'Something went wrong!', + \ }, + \ ]}) + +Execute(ale#lsp#response#ReadDiagnostics() should handle multiple messages): + AssertEqual ['filename.ts', [ + \ { + \ 'type': 'E', + \ 'message': 'Something went wrong!', + \ 'lnum': 1, + \ 'col': 3, + \ 'end_lnum': 1, + \ 'end_col': 3, + \ }, + \ { + \ 'type': 'W', + \ 'message': 'A warning', + \ 'lnum': 2, + \ 'col': 5, + \ 'end_lnum': 2, + \ 'end_col': 5, + \ }, + \ ]], + \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ + \ { + \ 'range': Range(0, 2, 0, 2), + \ 'message': 'Something went wrong!', + \ }, + \ { + \ 'severity': 2, + \ 'range': Range(1, 4, 1, 4), + \ 'message': 'A warning', + \ }, + \ ]}) diff --git a/test/test_lsp_client_messages.vader b/test/test_lsp_client_messages.vader deleted file mode 100644 index d6b398c4..00000000 --- a/test/test_lsp_client_messages.vader +++ /dev/null @@ -1,254 +0,0 @@ -Before: - let g:ale_lsp_next_message_id = 1 - - function CheckMessage(message, expected_method_name, ...) abort - if a:0 > 1 - throw 'Too many arguments!' - endif - - let l:match = matchlist(a:message, '\v^Content-Length: (\d+)' . "\r\n\r\n" . '(.+)$') - - if empty(l:match) - Assert 0, 'Invalid message format: ' . a:message - endif - - if strlen(l:match[2]) < str2nr(l:match[1]) - Assert 0, 'Invalid Content-Length (' . l:match[1] . ') :' . a:message - endif - - let l:expected_json = { - \ 'id': g:ale_lsp_next_message_id - 1, - \ 'jsonrpc': '2.0', - \ 'method': a:expected_method_name, - \} - - if a:0 > 0 - let l:expected_json.params = a:1 - endif - - AssertEqual l:expected_json, json_decode(l:match[2]) - endfunction - - function Range(start_line, start_char, end_line, end_char) abort - return { - \ 'start': {'line': a:start_line, 'character': a:start_char}, - \ 'end': {'line': a:end_line, 'character': a:end_char}, - \} - endfunction - -After: - delfunction CheckMessage - delfunction Range - -Execute(GetNextMessageID() should increment appropriately): - " We should get the initial ID, and increment a bit. - AssertEqual 1, ale#lsp#GetNextMessageID() - AssertEqual 2, ale#lsp#GetNextMessageID() - AssertEqual 3, ale#lsp#GetNextMessageID() - - " Set the maximum ID. - let g:ale_lsp_next_message_id = 9223372036854775807 - - " When we hit the maximum ID, the next ID afterwards should be 1. - AssertEqual 9223372036854775807, ale#lsp#GetNextMessageID() - AssertEqual 1, ale#lsp#GetNextMessageID() - -Execute(ale#lsp#CreateMessage() should create an appropriate message): - " 71 is the size in bytes for UTF-8, not the number of characters. - AssertEqual - \ "Content-Length: 71\r\n\r\n" - \ . '{"id":1,"jsonrpc":"2.0","method":"someMethod","params":{"foo":"barÜ"}}', - \ ale#lsp#CreateMessage('someMethod', {'foo': 'barÜ'}) - " Check again to ensure that we use the next ID. - AssertEqual - \ "Content-Length: 71\r\n\r\n" - \ . '{"id":2,"jsonrpc":"2.0","method":"someMethod","params":{"foo":"barÜ"}}', - \ ale#lsp#CreateMessage('someMethod', {'foo': 'barÜ'}) - -Execute(ale#lsp#ReadMessage() should read messages correctly): - AssertEqual - \ {'id': 2, 'jsonrpc': '2.0', 'result': {'foo': 'barÜ'}}, - \ ale#lsp#ReadMessage( - \ "Content-Length: 49\r\n\r\n" - \ . '{"id":2,"jsonrpc":"2.0","result":{"foo":"barÜ"}}' - \ ) - -Execute(ale#lsp#message#Initialize() should return correct messages): - call CheckMessage( - \ ale#lsp#message#Initialize(123, '/foo/bar'), - \ 'initialize', - \ { - \ 'processId': 123, - \ 'rootUri': '/foo/bar', - \ 'capabilities': {}, - \ } - \) - -Execute(ale#lsp#message#Initialized() should return correct messages): - call CheckMessage(ale#lsp#message#Initialized(), 'initialized') - -Execute(ale#lsp#message#Shutdown() should return correct messages): - call CheckMessage(ale#lsp#message#Shutdown(), 'shutdown') - -Execute(ale#lsp#message#Exit() should return correct messages): - call CheckMessage(ale#lsp#message#Exit(), 'exit') - -Execute(ale#lsp#message#DidOpen() should return correct messages): - call CheckMessage( - \ ale#lsp#message#DidOpen('/foo/bar', 'typescript', 123, 'foobar'), - \ 'textDocument/didOpen', - \ { - \ 'textDocument': { - \ 'uri': '/foo/bar', - \ 'languageId': 'typescript', - \ 'version': 123, - \ 'text': 'foobar', - \ }, - \ } - \) - -Execute(ale#lsp#message#DidChange() should return correct messages): - call CheckMessage( - \ ale#lsp#message#DidChange('/foo/bar', 123, 'foobar'), - \ 'textDocument/didChange', - \ { - \ 'textDocument': { - \ 'uri': '/foo/bar', - \ 'version': 123, - \ }, - \ 'contentChanges': [{'text': 'foobar'}] - \ } - \) - -Execute(ale#lsp#message#DidSave() should return correct messages): - call CheckMessage( - \ ale#lsp#message#DidSave('/foo/bar'), - \ 'textDocument/didSave', - \ { - \ 'textDocument': { - \ 'uri': '/foo/bar', - \ }, - \ } - \) - -Execute(ale#lsp#message#DidClose() should return correct messages): - call CheckMessage( - \ ale#lsp#message#DidClose('/foo/bar'), - \ 'textDocument/didClose', - \ { - \ 'textDocument': { - \ 'uri': '/foo/bar', - \ }, - \ } - \) - -Execute(ale#lsp#ReadDiagnostics() should handle errors): - AssertEqual ['filename.ts', [ - \ { - \ 'type': 'E', - \ 'message': 'Something went wrong!', - \ 'lnum': 3, - \ 'col': 11, - \ 'end_lnum': 5, - \ 'end_col': 16, - \ 'nr': 'some-error', - \ } - \ ]], - \ ale#lsp#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ - \ { - \ 'severity': 1, - \ 'range': Range(2, 10, 4, 15), - \ 'code': 'some-error', - \ 'message': 'Something went wrong!', - \ }, - \ ]}) - -Execute(ale#lsp#ReadDiagnostics() should handle warnings): - AssertEqual ['filename.ts', [ - \ { - \ 'type': 'W', - \ 'message': 'Something went wrong!', - \ 'lnum': 2, - \ 'col': 4, - \ 'end_lnum': 2, - \ 'end_col': 4, - \ 'nr': 'some-warning', - \ } - \ ]], - \ ale#lsp#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ - \ { - \ 'severity': 2, - \ 'range': Range(1, 3, 1, 3), - \ 'code': 'some-warning', - \ 'message': 'Something went wrong!', - \ }, - \ ]}) - -Execute(ale#lsp#ReadDiagnostics() should treat messages with missing severity as errors): - AssertEqual ['filename.ts', [ - \ { - \ 'type': 'E', - \ 'message': 'Something went wrong!', - \ 'lnum': 3, - \ 'col': 11, - \ 'end_lnum': 5, - \ 'end_col': 16, - \ 'nr': 'some-error', - \ } - \ ]], - \ ale#lsp#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ - \ { - \ 'range': Range(2, 10, 4, 15), - \ 'code': 'some-error', - \ 'message': 'Something went wrong!', - \ }, - \ ]}) - -Execute(ale#lsp#ReadDiagnostics() should handle messages without codes): - AssertEqual ['filename.ts', [ - \ { - \ 'type': 'E', - \ 'message': 'Something went wrong!', - \ 'lnum': 3, - \ 'col': 11, - \ 'end_lnum': 5, - \ 'end_col': 16, - \ } - \ ]], - \ ale#lsp#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ - \ { - \ 'range': Range(2, 10, 4, 15), - \ 'message': 'Something went wrong!', - \ }, - \ ]}) - -Execute(ale#lsp#ReadDiagnostics() should handle multiple messages): - AssertEqual ['filename.ts', [ - \ { - \ 'type': 'E', - \ 'message': 'Something went wrong!', - \ 'lnum': 1, - \ 'col': 3, - \ 'end_lnum': 1, - \ 'end_col': 3, - \ }, - \ { - \ 'type': 'W', - \ 'message': 'A warning', - \ 'lnum': 2, - \ 'col': 5, - \ 'end_lnum': 2, - \ 'end_col': 5, - \ }, - \ ]], - \ ale#lsp#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [ - \ { - \ 'range': Range(0, 2, 0, 2), - \ 'message': 'Something went wrong!', - \ }, - \ { - \ 'severity': 2, - \ 'range': Range(1, 4, 1, 4), - \ 'message': 'A warning', - \ }, - \ ]})