diff --git a/README.md b/README.md index 58dced6..960889f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ Shim for the language server protocol developed by Microsoft. The protocol can b ## Configuration +First you need to install a language server. An example of installing one might be: + +```shell +$ go get github.com/sourcegraph/go-langserver/langserver/cmd/langserver-go +``` + +A more complete set of language servers can be found here: https://github.com/Microsoft/language-server-protocol/wiki/Protocol-Implementations + You will need to put this somewhere that is sourced on startup. ```vim diff --git a/autoload/langserver/api/textDocument.vim b/autoload/langserver/api/textDocument.vim new file mode 100644 index 0000000..25f5e02 --- /dev/null +++ b/autoload/langserver/api/textDocument.vim @@ -0,0 +1,9 @@ +"" +function! langserver#api#textDocument#definition(request) abort + return langserver#goto#goto_defintion( + \ langserver#util#get_lsp_id(), + \ a:request.result.uri, + \ a:request.result.range, + \ {}, + \ ) +endfunction diff --git a/autoload/langserver/callbacks.vim b/autoload/langserver/callbacks.vim index d09aae7..05ccde7 100644 --- a/autoload/langserver/callbacks.vim +++ b/autoload/langserver/callbacks.vim @@ -3,11 +3,7 @@ function! langserver#callbacks#on_stdout(id, data, event) abort endfunction function! langserver#callbacks#on_stderr(id, data, event) abort - echom 'stderr ...' call langserver#log#response(a:id, a:data, a:event) - - echom string(a:data) - echom '...stderr' endfunction function! langserver#callbacks#on_exit(id, status, event) abort @@ -31,14 +27,12 @@ function! langserver#callbacks#on_notification(id, data, event) abort call langserver#hover#callback(a:id, a:data, a:event) elseif l:last_topic ==? 'textDocument/didOpen' call langserver#documents#callback_did_open(a:id, a:data, a:event) + elseif l:last_topic ==? 'initialize' + call langserver#initialize#callback(a:id, a:data, a:event) elseif l:last_topic ==? 'workspace/symbol' call langserver#symbol#workspace#callback(a:id, a:data, a:event) else - " if langserver#extension#command#callback(a:id, a:data, a:event) - " call langserver#log#log('debug', 'Handled: ' . l:last_topic) - " else - call langserver#log#log('warning', 'LAST REQUEST: ' . l:last_topic, v:true) - " endif + call langserver#log#log('warning', 'No callback registered for: ' . l:last_topic, v:true) endif elseif has_key(a:data, 'request') echom 'notification...' diff --git a/autoload/langserver/client.vim b/autoload/langserver/client.vim index a6e3371..2e64e54 100644 --- a/autoload/langserver/client.vim +++ b/autoload/langserver/client.vim @@ -10,6 +10,17 @@ let s:lsp_text_document_sync_kind_incremental = 2 function! s:_on_lsp_stdout(id, data, event) abort if has_key(s:lsp_clients, a:id) + " let s:lsp_clients[l:lsp_client_id] = { + " \ 'id': l:lsp_client_id, + " \ 'opts': a:opts, + " \ 'req_seq': 0, + " \ 'on_notifications': {}, + " \ 'stdout': { + " \ 'max_buffer_size': l:max_buffer_size, + " \ 'buffer': '', + " \ 'next_token': s:lsp_token_type_contentlength, + " \ }, + " \ } let l:client = s:lsp_clients[a:id] let l:client.stdout.buffer .= join(a:data, "\n") @@ -146,6 +157,10 @@ function! s:lsp_send_request(id, opts) abort " opts = { method, params?, on_noti let l:msg.params = a:opts.params endif + if has_key(a:opts, 'result') + let l:msg.result = a:opts.result + endif + let l:json = json_encode(l:msg) let l:req_data = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json @@ -175,6 +190,14 @@ function! s:lsp_get_last_request_id(id) abort endfunction function! s:lsp_is_error(notification) abort + " if type(a:notification) != type({}) + " return v:true + " endif + + " if !has_key(a:notification, 'response') || !has_key(a:notification.response, 'result') + " return v:true + " endif + return has_key(a:notification, 'error') endfunction diff --git a/autoload/langserver/default.vim b/autoload/langserver/default.vim index 404e4ab..a799a88 100644 --- a/autoload/langserver/default.vim +++ b/autoload/langserver/default.vim @@ -2,10 +2,31 @@ let s:langserver_executabe = 'langserver-go' "" " Get the default command for starting the server -function! langserver#default#cmd() abort - if has_key(g:langserver_executables, &filetype) - return g:langserver_executables[&filetype]['cmd'] +function! langserver#default#cmd(...) abort + if a:0 > 0 + let l:filetype_key = langserver#util#get_executable_key(a:1) else - return [-1] + let l:filetype_key = langserver#util#get_executable_key(&filetype) endif + + let l:bad_cmd = [-1] + + if has_key(g:langserver_executables, l:filetype_key) + " Has to be uppercase because of function naming + " Sorry for mixed case :/ + let l:TmpCmd = g:langserver_executables[l:filetype_key]['cmd'] + + if type(l:TmpCmd) == type([]) + return l:TmpCmd + elseif type(l:TmpCmd) == type(function('tr')) + let l:result = l:TmpCmd() + if type(l:result) == type([]) + return l:result + endif + endif + endif + + " If we didn't return anything, there was an error. + echoerr 'Please consult the documentation for how to configure the langserver' + return l:bad_cmd endfunction diff --git a/autoload/langserver/extension/command.vim b/autoload/langserver/extension/command.vim index bed1fe1..6963205 100644 --- a/autoload/langserver/extension/command.vim +++ b/autoload/langserver/extension/command.vim @@ -20,10 +20,9 @@ function! langserver#extension#command#callback(id, data, event) abort call langserver#client#send(a:id, { \ 'req_id': a:data.request.id, - \ 'method': 'fs/readFile', - \ 'params': { - \ 'result': l:response, - \ }, + \ 'method': l:method, + \ 'params': a:data.request.params, + \ 'result': l:response, \ }) return v:true endfunction diff --git a/autoload/langserver/extension/fs.vim b/autoload/langserver/extension/fs.vim index adf4ae6..fec21f9 100644 --- a/autoload/langserver/extension/fs.vim +++ b/autoload/langserver/extension/fs.vim @@ -7,5 +7,12 @@ function! langserver#extension#fs#readFille(filename) abort echom a:filename - return join(readfile(a:filename), "\n") + " let l:text = join(readfile(a:filename), "\n") + " echo system(['base64', join(readfile('/home/tj/test/lsp.py'), "\n")]) + if filereadable(a:filename) + return system(['base64', a:filename]) + else + call langserver#log#log('error', 'Unable to read file: ' . a:filename) + return '' + endif endfunction diff --git a/autoload/langserver/goto.vim b/autoload/langserver/goto.vim index 78ac311..8fade52 100644 --- a/autoload/langserver/goto.vim +++ b/autoload/langserver/goto.vim @@ -8,8 +8,14 @@ function! langserver#goto#callback(id, data, event) abort if has_key(a:data, 'response') if type(a:data['response']['result']) == type([]) let l:parsed_data = a:data['response']['result'][0] - else - let l:parsed_data = a:data['response']['result'] + elseif type(a:data['response']['result']) == type({}) + " Check if we have an empty dictionary + if empty(a:data['response']['result']) + call langserver#log#log('warning', 'No definition found for: ' . string(a:data['request']), v:true) + return + else + let l:parsed_data = a:data['response']['result'] + endif endif else return @@ -77,8 +83,11 @@ function! langserver#goto#goto_defintion(name, uri, range_dict, options) abort " \ a:range_dict['start']['line'] + 1, " \ a:range_dict['start']['character'] + 1, " \ ) - execute(printf('norm! %sG%s|', + let l:action = printf('norm! %sG%s|', \ a:range_dict['start']['line'] + 1, \ a:range_dict['start']['character'] + 1, - \ )) + \ ) + execute(l:action) + + return l:action endfunction diff --git a/autoload/langserver/initialize.vim b/autoload/langserver/initialize.vim index 7e7702b..c4a2326 100644 --- a/autoload/langserver/initialize.vim +++ b/autoload/langserver/initialize.vim @@ -30,6 +30,19 @@ function! langserver#initialize#get_client_capabilities() abort return {} endfunction +function! langserver#initialize#callback(id, data, event) abort + if langserver#client#is_error(a:data.response) + call langserver#log#log('error', + \ 'Could not connect to: ' . string(a:id) . "\n" . + \ 'Message was: ' . string(a:data), + \ v:true, + \ ) + else + call langserver#initialize#response(a:id, a:data.response.result) + call langserver#log#log('info', 'Succesfully connected to: ' . string(a:id), v:true) + endif +endfunction + "" " Handle the response of the server. " This message details the capabilities of the language server. @@ -47,7 +60,7 @@ function! langserver#initialize#response(name, response) abort if has_key(a:response, 'completionProvider') let l:complete_opt_resolve = get(a:response['completionProvider'], 'resolveProvider', v:false) - let l:complete_opt_trigger = get(a:resposne['completionProvider'], 'triggerCharacters', []) + let l:complete_opt_trigger = get(a:response['completionProvider'], 'triggerCharacters', []) call langserver#capabilities#set_completion_provider(a:name, l:complete_opt_resolve, l:complete_opt_trigger) endif diff --git a/autoload/langserver/log.vim b/autoload/langserver/log.vim index 26186e3..91c9676 100644 --- a/autoload/langserver/log.vim +++ b/autoload/langserver/log.vim @@ -2,117 +2,120 @@ let s:log_file = expand('~/langserver-vim.log') let s:current_level = 4 let s:log_level_map = { - \ 'error': 0, - \ 'warning': 1, - \ 'info': 2, - \ 'debug': 3, - \ 'micro': 4, - \ } + \ 'error': 0, + \ 'warning': 1, + \ 'info': 2, + \ 'debug': 3, + \ 'micro': 4, + \ } let s:clear_log = v:true "" " Logging helper function! langserver#log#log(level, message, ...) abort - if s:clear_log - call writefile([], s:log_file, '') - let s:clear_log = v:false - endif - - if a:0 > 0 - let l:echo_choice = a:1 - else - let l:echo_choice = v:false - endif - - let l:numeric_level = s:log_level_map[a:level] - - if type(a:message) == type('') - let l:msg = [a:message] - elseif type(a:message) == type({}) - let l:msg = [string(a:message)] - elseif type(a:message) != type([]) - " TODO: Handle other types of messages? - else - let l:msg = a:message - endif - - let l:final_msg = [] - for l:item in l:msg - call add(l:final_msg, printf('%5s: %s', - \ a:level, - \ l:item, - \ )) - endfor - - - if l:numeric_level < s:current_level - if l:echo_choice - echom string(l:final_msg) - endif - - call writefile(l:final_msg, s:log_file, 'a') - endif + if s:clear_log + call writefile([], s:log_file, '') + let s:clear_log = v:false + endif + + if a:0 > 0 + let l:echo_choice = a:1 + else + let l:echo_choice = v:false + endif + + let l:numeric_level = s:log_level_map[a:level] + + if type(a:message) == type('') + let l:msg = [a:message] + elseif type(a:message) == type({}) + let l:msg = [string(a:message)] + elseif type(a:message) != type([]) + " TODO: Handle other types of messages? + else + let l:msg = a:message + endif + + let l:final_msg = [] + for l:item in l:msg + call add(l:final_msg, printf('%5s: %s', + \ a:level, + \ l:item, + \ )) + endfor + + + if l:numeric_level < s:current_level + if l:echo_choice + echom string(l:final_msg) + endif + + call writefile(l:final_msg, s:log_file, 'a') + endif endfunction "" " Log response helper function! langserver#log#response(id, data, event) abort - let g:last_response = a:data - - if type(a:data) != type({}) - call langserver#log#log('debug', - \ printf('(%3s:%15s): %s', - \ a:id, - \ a:event, - \ string(a:data) - \ ), - \ langserver#util#debug(), - \ ) - return - endif - - if has_key(a:data, 'response') - call langserver#log#log('debug', - \ printf('(%3s:%15s): Response -> M(%20s), D(%s)', - \ a:id, - \ a:event, - \ string(a:data.request.method), - \ string(a:data.response.result), - \ ), - \ langserver#util#debug(), - \ ) - elseif has_key(a:data, 'request') - call langserver#log#log('debug', - \ printf('(%3s:%15s): Request -> M(%20s), D(%s)', - \ a:id, - \ a:event, - \ string(a:data.request.method), - \ string(a:data.request.params), - \ ), - \ langserver#util#debug(), - \ ) - else - call langserver#log#log('debug', - \ printf('(%3s:%15s): Unknown -> D(%s)', - \ a:id, - \ a:event, - \ string(a:data), - \ ), - \ langserver#util#debug(), - \ ) - endif + let g:last_response = a:data + + if type(a:data) != type({}) + call langserver#log#log('debug', + \ printf('(%3s:%15s): %s', + \ a:id, + \ a:event, + \ string(a:data) + \ ), + \ langserver#util#debug(), + \ ) + return + endif + + if has_key(a:data, 'response') && has_key(a:data, 'request') + \ && has_key(a:data.response, 'result') && has_key(a:data.request, 'method') + call langserver#log#log('debug', + \ printf('(%3s:%15s): Response -> M(%20s), D(%s)', + \ a:id, + \ a:event, + \ string(a:data.request.method), + \ string(a:data.response.result), + \ ), + \ langserver#util#debug(), + \ ) + elseif has_key(a:data, 'request') && type(a:data.request) == type({}) + call langserver#log#log('debug', + \ printf('(%3s:%15s): Request -> M(%20s), D(%s)', + \ a:id, + \ a:event, + \ string(a:data.request.method), + \ string(a:data.request.params), + \ ), + \ langserver#util#debug(), + \ ) + else + call langserver#log#log('debug', + \ printf('(%3s:%15s): Unknown -> D(%s)', + \ a:id, + \ a:event, + \ string(a:data), + \ ), + \ langserver#util#debug(), + \ ) + endif endfunction "" " Log only at debug level function! langserver#log#callback(id, data, event) abort - call langserver#log#log('debug', - \ printf('(%3s:%15s): %s', - \ a:id, - \ a:event, - \ string(a:data) - \)) + call langserver#log#log('debug', + \ printf('(%3s:%15s): %s', + \ a:id, + \ a:event, + \ string(a:data) + \ ), + \ v:false + \ ) endfunction "" @@ -120,10 +123,10 @@ endfunction function! langserver#log#server_request(id, data, event) abort call langserver#log#log('info', \ printf('(%3s:%15s): %s', - \ a:id, - \ a:event, - \ string(a:data) - \ )) + \ a:id, + \ a:event, + \ string(a:data) + \ )) endfunction function! langserver#log#pretty_print(json_dict) abort diff --git a/autoload/langserver/util.vim b/autoload/langserver/util.vim index ccbaa62..7d5043c 100644 --- a/autoload/langserver/util.vim +++ b/autoload/langserver/util.vim @@ -71,24 +71,24 @@ endfunction " @key code TODO " @key source TODO function! langserver#util#get_diagnostic(start, end, message, options) abort - let return_dict = { + let l:return_dict = { \ 'range': langserver#util#get_range(a:start, a:end), \ 'messsage': a:message \ } if has_key(a:options, 'severity') - let return_dict['severity'] = a:options['severity'] + let l:return_dict['severity'] = a:options['severity'] endif if has_key(a:options, 'code') - let return_dict['code'] = a:options['code'] + let l:return_dict['code'] = a:options['code'] endif if has_key(a:options, 'source') - let return_dict['source'] = a:options['source'] + let l:return_dict['source'] = a:options['source'] endif - return return_dict + return l:return_dict endfunction "" @@ -103,13 +103,13 @@ endfunction " @param command (str): The identifier of the actual command handler " @param arguments (Optional[list]): Optional list of arguments passed to the command function! langserver#util#get_command(title, command, ...) abort - let return_dict = { + let l:return_dict = { \ 'title': a:title, \ 'command': a:command, \ } if a:0 > 1 - let return_dict['arguments'] = a:1 + let l:return_dict['arguments'] = a:1 endif endfunction @@ -137,7 +137,7 @@ endfunction " TODO: Figure out a better way to do this. " @param function! langserver#util#get_workspace_edit(uri, edit) abort - let my_dict = { + let l:my_dict = { \ 'uri': 'edit', \ } endfunction @@ -209,38 +209,60 @@ endfunction " Parse the stdin of a server function! langserver#util#parse_message(message) abort if type(a:message) ==# type([]) - let data = join(a:message, '') + let l:data = join(a:message, '') elseif type(a:message) ==# type('') - let data = a:message + let l:data = a:message else endif - let parsed = {} - if data =~? '--> request' - let parsed['type'] = 'request' - elseif data =~? '<-- result' - let parsed['type'] = 'result' + let l:parsed = {} + if l:data =~? '--> request' + let l:parsed['type'] = 'request' + elseif l:data =~? '<-- result' + let l:parsed['type'] = 'result' else - let parsed['type'] = 'info' + let l:parsed['type'] = 'info' endif - let data = substitute(data, '--> request #\w*: ', '', 'g') - let data = substitute(data, '<-- result #\w*: ', '', 'g') + let l:data = substitute(l:data, '--> request #\w*: ', '', 'g') + let l:data = substitute(l:data, '<-- result #\w*: ', '', 'g') - if parsed['type'] ==# 'request' || parsed['type'] ==# 'result' - let data = substitute(data, '^\(\S*\):', '"\1":', 'g') - let data = '{' . data . '}' - let data = json_decode(data) + if l:parsed['type'] ==# 'request' || l:parsed['type'] ==# 'result' + let l:data = substitute(l:data, '^\(\S*\):', '"\1":', 'g') + let l:data = '{' . l:data . '}' + let l:data = json_decode(l:data) endif - let parsed['data'] = data - return parsed + let l:parsed['data'] = l:data + return l:parsed endfunction function! langserver#util#debug() abort return v:false endfunction +function! langserver#util#get_executable_key(...) abort + if a:0 > 0 + let l:file_type = a:1 + else + let l:file_type = &filetype + endif + + if !exists('g:langserver_executables') + echoerr '`g:langserver_executables` was not defined' + return '' + endif + + for l:k in keys(g:langserver_executables) + if index(split(l:k, ','), l:file_type) >= 0 + return l:k + endif + endfor + + echoerr 'Unsupported filetype: ' . l:file_type + return '' +endfunction + function! langserver#util#get_lsp_id() abort let g:lsp_id_map = get(g:, 'lsp_id_map', {}) diff --git a/doc/langserver.txt b/doc/langserver.txt index 0127287..007d2b6 100644 --- a/doc/langserver.txt +++ b/doc/langserver.txt @@ -19,8 +19,19 @@ CONFIGURATION *langserver-configuration* Define a dictionary like this in your vimrc. > + function! GetMyJSCommand() abort + return ['executable', 'options'] + endfunction + let g:langserver_executables = { - \ 'go': ['langserver-go', '-trace', '-logfile', expand('~/Desktop/langserver-go.log')], + \ 'go': { + \ 'name': 'sourcegraph/langserver-go', + \ 'cmd': ['langserver-go', '-trace', '-logfile', expand('~/Desktop/langserver-go.log')], + \ }, + \ 'javascript,typescript': { + \ 'name': 'myjsserver', + \ 'cmd': function('GetMyJSCommand'), + \ }, \ } TODO: Mappings @@ -47,6 +58,11 @@ Begin with calling: > call langserver#start({}) < +or: > + + :LSPStart +> + TODO: Opening a file TODO: Goto definition TODO: Hover diff --git a/tests/test_defaults.vader b/tests/test_defaults.vader new file mode 100644 index 0000000..565d4cb --- /dev/null +++ b/tests/test_defaults.vader @@ -0,0 +1,66 @@ +Execute (Testing executable configuration): + let g:__temp_exec = get(g:, 'langserver_executables', {}) + let g:langserver_executables = { + \ 'go': { + \ 'name': 'sourcegraph/langserver-go', + \ 'cmd': ['langserver-go', '-trace', '-logfile', expand('~/Desktop/langserver-go.log')], + \ }, + \ 'python': { + \ 'name': 'sourcegraph/python-langserver', + \ 'cmd': [expand('~/bin/python-langserver/python-langserver.py')], + \ }, + \ 'javascript,typescript,jsx,tsx': { + \ 'name': 'javascript-typescript', + \ 'cmd': [], + \ }, + \ } + + AssertEqual 'javascript,typescript,jsx,tsx', langserver#util#get_executable_key('jsx') + + let g:langserver_executables = g:__temp_exec + + +Execute (Testing executable function as cmd): + let g:__temp_exec = get(g:, 'langserver_executables', {}) + + let g:__vader_test__global_var = 'not updated' + function! s:set_global_var() abort + let g:__vader_test__global_var = 'updated' + return ['executable', 'options'] + endfunction + + let g:langserver_executables = { + \ 'example': { + \ 'name': 'sourcegraph/langserver-go', + \ 'cmd': function('s:set_global_var'), + \ }, + \ } + + AssertEqual ['executable', 'options'], langserver#default#cmd('example') + AssertEqual g:__vader_test__global_var, 'updated' + + let g:langserver_executables = g:__temp_exec + + +Execute (Testing executable function as cmd with cs list): + let g:__temp_exec = get(g:, 'langserver_executables', {}) + + let g:__vader_test__global_var = 'not updated' + function! s:set_global_var() abort + let g:__vader_test__global_var = 'updated' + return ['executable', 'options'] + endfunction + + let g:langserver_executables = { + \ 'example,this,that,the_other': { + \ 'name': 'sourcegraph/langserver-go', + \ 'cmd': function('s:set_global_var'), + \ }, + \ } + + AssertEqual ['executable', 'options'], langserver#default#cmd('example') + AssertEqual g:__vader_test__global_var, 'updated' + + let g:langserver_executables = g:__temp_exec + + diff --git a/tests/test_goto.vader b/tests/test_goto.vader new file mode 100644 index 0000000..7e7e705 --- /dev/null +++ b/tests/test_goto.vader @@ -0,0 +1,42 @@ + +Given vim (Goto position): + let this = 'that' + + " A comment here + " A comment there + " Here a comment, there a comment + + " Everywhere a comment + +Do (Move): + 2j + +Execute (Call goto): + let g:test_request = { + \ 'result': { + \ 'uri': 'file://' . expand('%'), + \ 'range': { + \ 'start': {'line': 0, 'character': 4}, + \ 'end': {'line': 0, 'character': 8}, + \ }, + \ }} + + Log g:test_request.result.uri + + let cmd = langserver#api#textDocument#definition(g:test_request) + AssertEqual cmd, 'norm! 1G5|' + +" I Would like to use these but it doesn't seem to work. +" Then (delete character): +" x + +" Expect (Line to be deleted): +" let his = 'that' + +" " A comment here +" " A comment there +" " Here a comment, there a comment + +" " Everywhere a comment + +