diff options
author | w0rp <devw0rp@gmail.com> | 2017-05-08 22:18:28 +0100 |
---|---|---|
committer | w0rp <devw0rp@gmail.com> | 2017-05-08 22:18:28 +0100 |
commit | 28c6ec9cad3064966ff70c9da95c96364118eb57 (patch) | |
tree | 6d970e9455670827dced35eb59bcf62b03282775 /autoload | |
parent | cd79ced839fa2a5c3fc407d7cbe0cdf6734d17da (diff) | |
download | ale-28c6ec9cad3064966ff70c9da95c96364118eb57.zip |
#517 - Implement LSP chunked message parsing, sending messages to sockets, and callbacks
Diffstat (limited to 'autoload')
-rw-r--r-- | autoload/ale/lsp.vim | 159 | ||||
-rw-r--r-- | autoload/ale/lsp/message.vim | 43 | ||||
-rw-r--r-- | autoload/ale/lsp/response.vim | 44 |
3 files changed, 174 insertions, 72 deletions
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 <devw0rp@gmail.com> " 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 - if a:0 > 1 - throw 'Too many arguments!' - endif +" 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': ale#lsp#GetNextMessageID(), + \ 'id': v:null, \ 'jsonrpc': '2.0', - \ 'method': a:method_name, + \ 'method': a:message[1], \} - if a:0 > 0 - let l:obj.params = a: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 '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#ReadMessage(data) abort - let l:header_end_index = match(a:data, "\r\n\r\n") +function! ale#lsp#ReadMessageData(data) abort + let l:response_list = [] + let l:remainder = a:data - if l:header_end_index < 0 - throw 'Invalid messaage: ' . string(a:data) - endif + while 1 + " Look for the end of the HTTP headers + let l:body_start_index = matchend(l:remainder, "\r\n\r\n") - return json_decode(a:data[l:header_end_index + 4:]) -endfunction + if l:body_start_index < 0 + " No header end was found yet. + break + endif -" 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, - \} + " 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 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' + if empty(l:length_match) + throw "Invalid JSON-RPC header:\n" . l:header_data endif - if has_key(l:diagnostic, 'code') - let l:loclist_item.nr = l:diagnostic.code + " 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 - call add(l:loclist, l:loclist_item) + 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 + + if !a:message[0] && a:0 == 0 + throw 'A callback must be set for messages which are not notifications!' + endif + + let [l:id, l:data] = ale#lsp#CreateMessageData(a:message) + + let l:info = get(s:address_info_map, a:address, {}) + + if empty(l:info) + let l:info = { + \ 'data': '', + \ 'callback_map': {}, + \} + let s:address_info_map[a:address] = l:info + 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:info, 'channel') || ch_status(l:info.channel) !=# 'open' + let l:info.channnel = ch_open(a:address, { + \ 'mode': 'raw', + \ 'waittime': 0, + \ 'callback': 's:HandleMessage', + \}) + endif + + 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 <devw0rp@gmail.com> " 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 <devw0rp@gmail.com> +" 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 |