All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 let s:save_cpo = &cpoptions
4 let s:clients = {} " { client_id: ctx }
6 " Vars used by native lsp
9 function! s:create_context(client_id, opts) abort
17 \ 'content-length': -1,
19 \ 'request_sequence': 0,
20 \ 'on_notifications': {},
23 let s:clients[a:client_id] = l:ctx
28 function! s:dispose_context(client_id) abort
30 if has_key(s:clients, a:client_id)
31 unlet s:clients[a:client_id]
36 function! s:on_stdout(id, data, event) abort
37 let l:ctx = get(s:clients, a:id, {})
43 let l:ctx['buffer'] .= a:data
46 if l:ctx['content-length'] < 0
47 " wait for all headers to arrive
48 let l:header_end_index = stridx(l:ctx['buffer'], "\r\n\r\n")
49 if l:header_end_index < 0
53 let l:headers = l:ctx['buffer'][:l:header_end_index - 1]
54 let l:ctx['content-length'] = s:get_content_length(l:headers)
55 if l:ctx['content-length'] < 0
56 " invalid content-length
57 call lsp#log('on_stdout', a:id, 'invalid content-length')
61 let l:ctx['buffer'] = l:ctx['buffer'][l:header_end_index + 4:] " 4 = len(\r\n\r\n)
64 if len(l:ctx['buffer']) < l:ctx['content-length']
65 " incomplete message, wait for next buffer to arrive
69 " we have full message
70 let l:response_str = l:ctx['buffer'][:l:ctx['content-length'] - 1]
71 let l:ctx['content-length'] = -1
74 let l:response = json_decode(l:response_str)
76 call lsp#log('s:on_stdout json_decode failed', v:exception)
79 let l:ctx['buffer'] = l:ctx['buffer'][len(l:response_str):]
81 if exists('l:response')
82 " call appropriate callbacks
83 let l:on_notification_data = { 'response': l:response }
84 if has_key(l:response, 'method') && has_key(l:response, 'id')
85 " it is a request from a server
86 let l:request = l:response
87 if has_key(l:ctx['opts'], 'on_request')
88 call l:ctx['opts']['on_request'](a:id, l:request)
90 elseif has_key(l:response, 'id')
91 " it is a request->response
92 if !(type(l:response['id']) == type(0) || type(l:response['id']) == type(''))
93 " response['id'] can be number | string | null based on the spec
94 call lsp#log('invalid response id. ignoring message', l:response)
97 if has_key(l:ctx['requests'], l:response['id'])
98 let l:on_notification_data['request'] = l:ctx['requests'][l:response['id']]
100 if has_key(l:ctx['opts'], 'on_notification')
101 " call client's on_notification first
103 call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
105 call lsp#log('s:on_stdout client option on_notification() error', v:exception, v:throwpoint)
108 if has_key(l:ctx['on_notifications'], l:response['id'])
109 " call lsp#client#send({ 'on_notification }) second
111 call l:ctx['on_notifications'][l:response['id']](a:id, l:on_notification_data, 'on_notification')
113 call lsp#log('s:on_stdout client request on_notification() error', v:exception, v:throwpoint)
115 unlet l:ctx['on_notifications'][l:response['id']]
117 if has_key(l:ctx['requests'], l:response['id'])
118 unlet l:ctx['requests'][l:response['id']]
120 call lsp#log('cannot find the request corresponding to response: ', l:response)
123 " it is a notification
124 if has_key(l:ctx['opts'], 'on_notification')
126 call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
128 call lsp#log('s:on_stdout on_notification() error', v:exception, v:throwpoint)
134 if empty(l:response_str)
135 " buffer is empty, wait for next message to arrive
141 function! s:get_content_length(headers) abort
142 for l:header in split(a:headers, "\r\n")
143 let l:kvp = split(l:header, ':')
145 if l:kvp[0] =~? '^Content-Length'
146 return str2nr(l:kvp[1], 10)
153 function! s:on_stderr(id, data, event) abort
154 let l:ctx = get(s:clients, a:id, {})
158 if has_key(l:ctx['opts'], 'on_stderr')
160 call l:ctx['opts']['on_stderr'](a:id, a:data, a:event)
162 call lsp#log('s:on_stderr exception', v:exception, v:throwpoint)
168 function! s:on_exit(id, status, event) abort
169 let l:ctx = get(s:clients, a:id, {})
173 if has_key(l:ctx['opts'], 'on_exit')
175 call l:ctx['opts']['on_exit'](a:id, a:status, a:event)
177 call lsp#log('s:on_exit exception', v:exception, v:throwpoint)
181 call s:dispose_context(a:id)
184 function! s:lsp_start(opts) abort
186 \ 'on_stdout': function('s:on_stdout'),
187 \ 'on_stderr': function('s:on_stderr'),
188 \ 'on_exit': function('s:on_exit'),
189 \ 'normalize': 'string'
191 if has_key(a:opts, 'env')
192 let l:opts.env = a:opts.env
195 if has_key(a:opts, 'cmd')
196 let l:client_id = lsp#utils#job#start(a:opts.cmd, l:opts)
197 elseif has_key(a:opts, 'tcp')
198 let l:client_id = lsp#utils#job#connect(a:opts.tcp, l:opts)
203 let l:ctx = s:create_context(l:client_id, a:opts)
204 let l:ctx['id'] = l:client_id
209 function! s:lsp_stop(id) abort
210 call lsp#utils#job#stop(a:id)
213 let s:send_type_request = 1
214 let s:send_type_notification = 2
215 let s:send_type_response = 3
216 function! s:lsp_send(id, opts, type) abort " opts = { id?, method?, result?, params?, on_notification }
217 let l:ctx = get(s:clients, a:id, {})
218 if empty(l:ctx) | return -1 | endif
220 let l:request = { 'jsonrpc': '2.0' }
222 if (a:type == s:send_type_request)
223 let l:ctx['request_sequence'] = l:ctx['request_sequence'] + 1
224 let l:request['id'] = l:ctx['request_sequence']
225 let l:ctx['requests'][l:request['id']] = l:request
226 if has_key(a:opts, 'on_notification')
227 let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification']
231 if has_key(a:opts, 'id')
232 let l:request['id'] = a:opts['id']
234 if has_key(a:opts, 'method')
235 let l:request['method'] = a:opts['method']
237 if has_key(a:opts, 'params')
238 let l:request['params'] = a:opts['params']
240 if has_key(a:opts, 'result')
241 let l:request['result'] = a:opts['result']
243 if has_key(a:opts, 'error')
244 let l:request['error'] = a:opts['error']
247 let l:json = json_encode(l:request)
248 let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json
250 call lsp#utils#job#send(a:id, l:payload)
252 if (a:type == s:send_type_request)
253 let l:id = l:request['id']
254 if get(a:opts, 'sync', 0) !=# 0
255 let l:timeout = get(a:opts, 'sync_timeout', -1)
256 if lsp#utils#_wait(l:timeout, {-> !has_key(l:ctx['requests'], l:request['id'])}, 1) == -1
257 throw 'lsp#client: timeout'
266 function! s:lsp_get_last_request_id(id) abort
267 return s:clients[a:id]['request_sequence']
270 function! s:lsp_is_error(obj_or_response) abort
271 let l:vt = type(a:obj_or_response)
273 return len(a:obj_or_response) > 0
274 elseif l:vt == type({})
275 return has_key(a:obj_or_response, 'error')
281 function! s:is_server_instantiated_notification(notification) abort
282 return !has_key(a:notification, 'request')
285 function! s:native_out_cb(cbctx, channel, response) abort
286 if !has_key(a:cbctx, 'ctx') | return | endif
287 let l:ctx = a:cbctx['ctx']
288 if has_key(a:response, 'method') && has_key(a:response, 'id')
289 " it is a request from a server
290 let l:request = a:response
291 if has_key(l:ctx['opts'], 'on_request')
292 call l:ctx['opts']['on_request'](l:ctx['id'], l:request)
294 elseif !has_key(a:response, 'id') && has_key(l:ctx['opts'], 'on_notification')
295 " it is a notification
296 let l:on_notification_data = { 'response': a:response }
298 call l:ctx['opts']['on_notification'](l:ctx['id'], l:on_notification_data, 'on_notification')
300 call lsp#log('s:native_notification_callback on_notification() error', v:exception, v:throwpoint)
305 function! s:native_err_cb(cbctx, channel, response) abort
306 if !has_key(a:cbctx, 'ctx') | return | endif
307 let l:ctx = a:cbctx['ctx']
308 if has_key(l:ctx['opts'], 'on_stderr')
310 call l:ctx['opts']['on_stderr'](l:ctx['id'], a:response, 'stderr')
312 call lsp#log('s:on_stderr exception', v:exception, v:throwpoint)
318 function! s:native_exit_cb(cbctx, channel, response) abort
319 if !has_key(a:cbctx, 'ctx') | return | endif
320 let l:ctx = a:cbctx['ctx']
321 if has_key(l:ctx['opts'], 'on_exit')
323 call l:ctx['opts']['on_exit'](l:ctx['id'], a:response, 'exit')
325 call lsp#log('s:on_exit exception', v:exception, v:throwpoint)
329 call s:dispose_context(l:ctx['id'])
334 function! lsp#client#start(opts) abort
335 if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
336 if has_key(a:opts, 'cmd')
338 let l:jobopt = { 'in_mode': 'lsp', 'out_mode': 'lsp', 'noblock': 1,
339 \ 'out_cb': function('s:native_out_cb', [l:cbctx]),
340 \ 'err_cb': function('s:native_err_cb', [l:cbctx]),
341 \ 'exit_cb': function('s:native_exit_cb', [l:cbctx]),
343 if has_key(a:opts, 'cwd') | let l:jobopt['cwd'] = a:opts['cwd'] | endif
344 if has_key(a:opts, 'env') | let l:jobopt['env'] = a:opts['env'] | endif
346 let l:jobid = s:jobidseq " jobid == clientid
347 call lsp#log_verbose('using native lsp client')
348 let l:job = job_start(a:opts['cmd'], l:jobopt)
349 if job_status(l:job) !=? 'run' | return -1 | endif
350 let l:ctx = s:create_context(l:jobid, a:opts)
351 let l:ctx['id'] = l:jobid
352 let l:ctx['job'] = l:job
353 let l:ctx['channel'] = job_getchannel(l:job)
354 let l:cbctx['ctx'] = l:ctx
356 elseif has_key(a:opts, 'tcp')
357 " add support for tcp
358 call lsp#log('tcp not supported when using native lsp client')
362 return s:lsp_start(a:opts)
365 function! lsp#client#stop(client_id) abort
366 if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
367 let l:ctx = get(s:clients, a:client_id, {})
368 if empty(l:ctx) | return | endif
369 call job_stop(l:ctx['job'])
371 return s:lsp_stop(a:client_id)
375 function! lsp#client#send_request(client_id, opts) abort
376 if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
377 let l:ctx = get(s:clients, a:client_id, {})
378 if empty(l:ctx) | return -1 | endif
380 " id shouldn't be passed to request as vim will overwrite it. refer to :h language-server-protocol
381 if has_key(a:opts, 'method') | let l:request['method'] = a:opts['method'] | endif
382 if has_key(a:opts, 'params') | let l:request['params'] = a:opts['params'] | endif
384 call ch_sendexpr(l:ctx['channel'], l:request, { 'callback': function('s:on_response_native', [l:ctx, l:request]) })
385 let l:ctx['requests'][l:request['id']] = l:request
386 if has_key(a:opts, 'on_notification')
387 let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification']
389 if get(a:opts, 'sync', 0) !=# 0
390 let l:timeout = get(a:opts, 'sync_timeout', -1)
391 if lsp#utils#_wait(l:timeout, {-> !has_key(l:ctx['requests'], l:request['id'])}, 1) == -1
392 throw 'lsp#client#send_request: timeout'
395 let l:ctx['request_sequence'] = l:request['id']
396 return l:request['id']
398 return s:lsp_send(a:client_id, a:opts, s:send_type_request)
402 function! s:on_response_native(ctx, request, channel, response) abort
403 " request -> response
404 let l:on_notification_data = { 'response': a:response, 'request': a:request }
405 if has_key(a:ctx['opts'], 'on_notification')
406 " call client's on_notification first
408 call a:ctx['opts']['on_notification'](a:ctx['id'], l:on_notification_data, 'on_notification')
410 call lsp#log('s:on_response_native client option on_notification() error', v:exception, v:throwpoint)
413 if has_key(a:ctx['on_notifications'], a:request['id'])
414 " call lsp#client#send({ 'on_notification' }) second
416 call a:ctx['on_notifications'][a:request['id']](a:ctx['id'], l:on_notification_data, 'on_notification')
418 call lsp#log('s:on_response_native client request on_notification() error', v:exception, v:throwpoint, a:request, a:response)
420 unlet a:ctx['on_notifications'][a:response['id']]
421 if has_key(a:ctx['requests'], a:response['id'])
422 unlet a:ctx['requests'][a:response['id']]
424 call lsp#log('cannot find the request corresponding to response: ', a:response)
429 function! lsp#client#send_notification(client_id, opts) abort
430 if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
431 let l:ctx = get(s:clients, a:client_id, {})
432 if empty(l:ctx) | return -1 | endif
434 if has_key(a:opts, 'method') | let l:request['method'] = a:opts['method'] | endif
435 if has_key(a:opts, 'params') | let l:request['params'] = a:opts['params'] | endif
436 call ch_sendexpr(l:ctx['channel'], l:request)
439 return s:lsp_send(a:client_id, a:opts, s:send_type_notification)
443 function! lsp#client#send_response(client_id, opts) abort
444 if g:lsp_use_native_client && lsp#utils#has_native_lsp_client()
445 let l:ctx = get(s:clients, a:client_id, {})
446 if empty(l:ctx) | return -1 | endif
448 if has_key(a:opts, 'id') | let l:request['id'] = a:opts['id'] | endif
449 if has_key(a:opts, 'result') | let l:request['result'] = a:opts['result'] | endif
450 if has_key(a:opts, 'error') | let l:request['error'] = a:opts['error'] | endif
452 call ch_sendexpr(l:ctx['channel'], l:request)
454 " vim only supports id as number and fails when string, hence add a try catch: https://github.com/vim/vim/issues/14091
455 call lsp#log('lsp#client#send_response error', v:exception, v:throwpoint,
456 \ has_key(l:request, 'id') && type(l:request['id']) != type(1))
460 return s:lsp_send(a:client_id, a:opts, s:send_type_response)
464 function! lsp#client#get_last_request_id(client_id) abort
465 return s:lsp_get_last_request_id(a:client_id)
468 function! lsp#client#is_error(obj_or_response) abort
469 return s:lsp_is_error(a:obj_or_response)
472 function! lsp#client#error_message(obj_or_response) abort
474 return a:obj_or_response['error']['data']['message']
478 return a:obj_or_response['error']['message']
481 return string(a:obj_or_response)
484 function! lsp#client#is_server_instantiated_notification(notification) abort
485 return s:is_server_instantiated_notification(a:notification)
490 let &cpoptions = s:save_cpo