mirror of
https://github.com/prabirshrestha/vim-lsp.git
synced 2025-12-14 20:35:59 +01:00
* fix comment
* fix indent
* support diagnostics in a floating window
* doc: add g:lsp_diagnostics_float_{cursor,delay}
* fix vint errors except unused variables
* suppress vint unused variable errors
* lint: use robust operator
336 lines
10 KiB
VimL
336 lines
10 KiB
VimL
let s:save_cpo = &cpoptions
|
|
set cpoptions&vim
|
|
|
|
let s:clients = {} " { client_id: ctx }
|
|
|
|
function! s:create_context(client_id, opts) abort
|
|
if a:client_id <= 0
|
|
return {}
|
|
endif
|
|
|
|
let l:ctx = {
|
|
\ 'opts': a:opts,
|
|
\ 'buffer': '',
|
|
\ 'content-length': -1,
|
|
\ 'requests': {},
|
|
\ 'request_sequence': 0,
|
|
\ 'on_notifications': {},
|
|
\ }
|
|
|
|
let s:clients[a:client_id] = l:ctx
|
|
|
|
return l:ctx
|
|
endfunction
|
|
|
|
function! s:dispose_context(client_id) abort
|
|
if a:client_id > 0
|
|
if has_key(s:clients, a:client_id)
|
|
unlet s:clients[a:client_id]
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
function! s:on_stdout(id, data, event) abort
|
|
let l:ctx = get(s:clients, a:id, {})
|
|
|
|
if empty(l:ctx)
|
|
return
|
|
endif
|
|
|
|
if empty(l:ctx['buffer'])
|
|
let l:ctx['buffer'] = join(a:data, "\n")
|
|
else
|
|
let l:ctx['buffer'] .= join(a:data, "\n")
|
|
endif
|
|
|
|
while 1
|
|
if l:ctx['content-length'] < 0
|
|
" wait for all headers to arrive
|
|
let l:header_end_index = stridx(l:ctx['buffer'], "\r\n\r\n")
|
|
if l:header_end_index < 0
|
|
" no headers found
|
|
return
|
|
endif
|
|
let l:headers = l:ctx['buffer'][:l:header_end_index - 1]
|
|
let l:ctx['content-length'] = s:get_content_length(l:headers)
|
|
if l:ctx['content-length'] < 0
|
|
" invalid content-length
|
|
call lsp#log('on_stdout', a:id, 'invalid content-length')
|
|
call s:lsp_stop(a:id)
|
|
return
|
|
endif
|
|
let l:ctx['buffer'] = l:ctx['buffer'][l:header_end_index + 4:] " 4 = len(\r\n\r\n)
|
|
endif
|
|
|
|
if len(l:ctx['buffer']) < l:ctx['content-length']
|
|
" incomplete message, wait for next buffer to arrive
|
|
return
|
|
endif
|
|
|
|
" we have full message
|
|
let l:response_str = l:ctx['buffer'][:l:ctx['content-length'] - 1]
|
|
let l:ctx['content-length'] = -1
|
|
|
|
try
|
|
let l:response = json_decode(l:response_str)
|
|
catch
|
|
call lsp#log('s:on_stdout json_decode failed', v:exception)
|
|
endtry
|
|
|
|
let l:ctx['buffer'] = l:ctx['buffer'][len(l:response_str):]
|
|
|
|
if exists('l:response')
|
|
" call appropriate callbacks
|
|
let l:on_notification_data = { 'response': l:response }
|
|
if has_key(l:response, 'method') && has_key(l:response, 'id')
|
|
" it is a request from a server
|
|
let l:request = l:response
|
|
if has_key(l:ctx['opts'], 'on_request')
|
|
call l:ctx['opts']['on_request'](a:id, l:request)
|
|
endif
|
|
elseif has_key(l:response, 'id')
|
|
" it is a request->response
|
|
if !(type(l:response['id']) == type(0) || type(l:response['id']) == type(''))
|
|
" response['id'] can be number | string | null based on the spec
|
|
call lsp#log('invalid response id. ignoring message', l:response)
|
|
continue
|
|
endif
|
|
if has_key(l:ctx['requests'], l:response['id'])
|
|
let l:on_notification_data['request'] = l:ctx['requests'][l:response['id']]
|
|
endif
|
|
if has_key(l:ctx['opts'], 'on_notification')
|
|
" call client's on_notification first
|
|
try
|
|
call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
|
|
catch
|
|
call lsp#log('s:on_stdout client option on_notification() error', v:exception)
|
|
endtry
|
|
endif
|
|
if has_key(l:ctx['on_notifications'], l:response['id'])
|
|
" call lsp#client#send({ 'on_notification }) second
|
|
try
|
|
call l:ctx['on_notifications'][l:response['id']](a:id, l:on_notification_data, 'on_notification')
|
|
catch
|
|
call lsp#log('s:on_stdout client request on_notification() error', v:exception, v:throwpoint)
|
|
endtry
|
|
unlet l:ctx['on_notifications'][l:response['id']]
|
|
endif
|
|
if has_key(l:ctx['requests'], l:response['id'])
|
|
unlet l:ctx['requests'][l:response['id']]
|
|
else
|
|
call lsp#log('cannot find the request corresponding to response: ', l:response)
|
|
endif
|
|
else
|
|
" it is a notification
|
|
if has_key(l:ctx['opts'], 'on_notification')
|
|
try
|
|
call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
|
|
catch
|
|
call lsp#log('s:on_stdout on_notification() error', v:exception)
|
|
endtry
|
|
endif
|
|
endif
|
|
endif
|
|
|
|
if empty(l:response_str)
|
|
" buffer is empty, wait for next message to arrive
|
|
return
|
|
endif
|
|
endwhile
|
|
endfunction
|
|
|
|
function! s:get_content_length(headers) abort
|
|
for l:header in split(a:headers, "\r\n")
|
|
let l:kvp = split(l:header, ':')
|
|
if len(l:kvp) == 2
|
|
if l:kvp[0] =~? '^Content-Length'
|
|
return str2nr(l:kvp[1], 10)
|
|
endif
|
|
endif
|
|
endfor
|
|
return -1
|
|
endfunction
|
|
|
|
function! s:on_stderr(id, data, event) abort
|
|
let l:ctx = get(s:clients, a:id, {})
|
|
if empty(l:ctx)
|
|
return
|
|
endif
|
|
if has_key(l:ctx['opts'], 'on_stderr')
|
|
try
|
|
call l:ctx['opts']['on_stderr'](a:id, a:data, a:event)
|
|
catch
|
|
call lsp#log('s:on_stderr exception', v:exception)
|
|
echom v:exception
|
|
endtry
|
|
endif
|
|
endfunction
|
|
|
|
function! s:on_exit(id, status, event) abort
|
|
let l:ctx = get(s:clients, a:id, {})
|
|
if empty(l:ctx)
|
|
return
|
|
endif
|
|
if has_key(l:ctx['opts'], 'on_exit')
|
|
try
|
|
call l:ctx['opts']['on_exit'](a:id, a:status, a:event)
|
|
catch
|
|
call lsp#log('s:on_exit exception', v:exception)
|
|
echom v:exception
|
|
endtry
|
|
endif
|
|
call s:dispose_context(a:id)
|
|
endfunction
|
|
|
|
function! s:lsp_start(opts) abort
|
|
if !has_key(a:opts, 'cmd')
|
|
return -1
|
|
endif
|
|
|
|
let l:client_id = async#job#start(a:opts.cmd, {
|
|
\ 'on_stdout': function('s:on_stdout'),
|
|
\ 'on_stderr': function('s:on_stderr'),
|
|
\ 'on_exit': function('s:on_exit'),
|
|
\ })
|
|
|
|
let l:ctx = s:create_context(l:client_id, a:opts)
|
|
let l:ctx['id'] = l:client_id
|
|
|
|
return l:client_id
|
|
endfunction
|
|
|
|
function! s:lsp_stop(id) abort
|
|
call async#job#stop(a:id)
|
|
endfunction
|
|
|
|
let s:send_type_request = 1
|
|
let s:send_type_notification = 2
|
|
let s:send_type_response = 3
|
|
function! s:lsp_send(id, opts, type) abort " opts = { id?, method?, result?, params?, on_notification }
|
|
let l:ctx = get(s:clients, a:id, {})
|
|
if empty(l:ctx)
|
|
return -1
|
|
endif
|
|
|
|
let l:request = { 'jsonrpc': '2.0' }
|
|
|
|
if (a:type == s:send_type_request)
|
|
let l:ctx['request_sequence'] = l:ctx['request_sequence'] + 1
|
|
let l:request['id'] = l:ctx['request_sequence']
|
|
let l:ctx['requests'][l:request['id']] = l:request
|
|
if has_key(a:opts, 'on_notification')
|
|
let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification']
|
|
endif
|
|
endif
|
|
|
|
if has_key(a:opts, 'id')
|
|
let l:request['id'] = a:opts['id']
|
|
endif
|
|
if has_key(a:opts, 'method')
|
|
let l:request['method'] = a:opts['method']
|
|
endif
|
|
if has_key(a:opts, 'params')
|
|
let l:request['params'] = a:opts['params']
|
|
endif
|
|
if has_key(a:opts, 'result')
|
|
let l:request['result'] = a:opts['result']
|
|
endif
|
|
if has_key(a:opts, 'error')
|
|
let l:request['error'] = a:opts['error']
|
|
endif
|
|
|
|
let l:json = json_encode(l:request)
|
|
let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json
|
|
|
|
call async#job#send(a:id, l:payload)
|
|
|
|
if (a:type == s:send_type_request)
|
|
let l:id = l:request['id']
|
|
if get(a:opts, 'sync', 0) !=# 0
|
|
let l:start_time = reltime()
|
|
|
|
let l:timeout = get(a:opts, 'sync_timeout', -1)
|
|
while has_key(l:ctx['requests'], l:request['id'])
|
|
if (reltimefloat(reltime(l:start_time)) * 1000) > l:timeout && l:timeout != -1
|
|
throw 'lsp#client: timeout'
|
|
endif
|
|
sleep 1m
|
|
endwhile
|
|
endif
|
|
return l:id
|
|
else
|
|
return 0
|
|
endif
|
|
endfunction
|
|
|
|
function! s:lsp_get_last_request_id(id) abort
|
|
return s:clients[a:id]['request_sequence']
|
|
endfunction
|
|
|
|
function! s:lsp_is_error(obj_or_response) abort
|
|
let l:vt = type(a:obj_or_response)
|
|
if l:vt == type('')
|
|
return len(a:obj_or_response) > 0
|
|
elseif l:vt == type({})
|
|
return has_key(a:obj_or_response, 'error')
|
|
endif
|
|
return 0
|
|
endfunction
|
|
|
|
|
|
function! s:is_server_instantiated_notification(notification) abort
|
|
return !has_key(a:notification, 'request')
|
|
endfunction
|
|
|
|
" public apis {{{
|
|
|
|
function! lsp#client#start(opts) abort
|
|
return s:lsp_start(a:opts)
|
|
endfunction
|
|
|
|
function! lsp#client#stop(client_id) abort
|
|
return s:lsp_stop(a:client_id)
|
|
endfunction
|
|
|
|
function! lsp#client#send_request(client_id, opts) abort
|
|
return s:lsp_send(a:client_id, a:opts, s:send_type_request)
|
|
endfunction
|
|
|
|
function! lsp#client#send_notification(client_id, opts) abort
|
|
return s:lsp_send(a:client_id, a:opts, s:send_type_notification)
|
|
endfunction
|
|
|
|
function! lsp#client#send_response(client_id, opts) abort
|
|
return s:lsp_send(a:client_id, a:opts, s:send_type_response)
|
|
endfunction
|
|
|
|
function! lsp#client#get_last_request_id(client_id) abort
|
|
return s:lsp_get_last_request_id(a:client_id)
|
|
endfunction
|
|
|
|
function! lsp#client#is_error(obj_or_response) abort
|
|
return s:lsp_is_error(a:obj_or_response)
|
|
endfunction
|
|
|
|
function! lsp#client#error_message(obj_or_response) abort
|
|
try
|
|
return a:obj_or_response['error']['data']['message']
|
|
catch
|
|
endtry
|
|
try
|
|
return a:obj_or_response['error']['message']
|
|
catch
|
|
endtry
|
|
return string(a:obj_or_response)
|
|
endfunction
|
|
|
|
function! lsp#client#is_server_instantiated_notification(notification) abort
|
|
return s:is_server_instantiated_notification(a:notification)
|
|
endfunction
|
|
|
|
" }}}
|
|
|
|
let &cpoptions = s:save_cpo
|
|
unlet s:save_cpo
|
|
" vim sw=4 ts=4 et
|