]> git.madduck.net Git - etc/vim.git/blob - autoload/lsp/client.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/vim-lsp/' content from commit 04428c92
[etc/vim.git] / autoload / lsp / client.vim
1 let s:save_cpo = &cpoptions
2 set cpoptions&vim
3
4 let s:clients = {} " { client_id: ctx }
5
6 " Vars used by native lsp
7 let s:jobidseq = 0
8
9 function! s:create_context(client_id, opts) abort
10     if a:client_id <= 0
11         return {}
12     endif
13
14     let l:ctx = {
15         \ 'opts': a:opts,
16         \ 'buffer': '',
17         \ 'content-length': -1,
18         \ 'requests': {},
19         \ 'request_sequence': 0,
20         \ 'on_notifications': {},
21         \ }
22
23     let s:clients[a:client_id] = l:ctx
24
25     return l:ctx
26 endfunction
27
28 function! s:dispose_context(client_id) abort
29     if a:client_id > 0
30         if has_key(s:clients, a:client_id)
31             unlet s:clients[a:client_id]
32         endif
33     endif
34 endfunction
35
36 function! s:on_stdout(id, data, event) abort
37     let l:ctx = get(s:clients, a:id, {})
38
39     if empty(l:ctx)
40         return
41     endif
42
43     let l:ctx['buffer'] .= a:data
44
45     while 1
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
50                 " no headers found
51                 return
52             endif
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')
58                 call s:lsp_stop(a:id)
59                 return
60             endif
61             let l:ctx['buffer'] = l:ctx['buffer'][l:header_end_index + 4:] " 4 = len(\r\n\r\n)
62         endif
63
64         if len(l:ctx['buffer']) < l:ctx['content-length']
65             " incomplete message, wait for next buffer to arrive
66             return
67         endif
68
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
72
73         try
74             let l:response = json_decode(l:response_str)
75         catch
76             call lsp#log('s:on_stdout json_decode failed', v:exception)
77         endtry
78
79         let l:ctx['buffer'] = l:ctx['buffer'][len(l:response_str):]
80
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)
89                 endif
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)
95                     continue
96                 endif
97                 if has_key(l:ctx['requests'], l:response['id'])
98                     let l:on_notification_data['request'] = l:ctx['requests'][l:response['id']]
99                 endif
100                 if has_key(l:ctx['opts'], 'on_notification')
101                     " call client's on_notification first
102                     try
103                         call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
104                     catch
105                         call lsp#log('s:on_stdout client option on_notification() error', v:exception, v:throwpoint)
106                     endtry
107                 endif
108                 if has_key(l:ctx['on_notifications'], l:response['id'])
109                     " call lsp#client#send({ 'on_notification }) second
110                     try
111                         call l:ctx['on_notifications'][l:response['id']](a:id, l:on_notification_data, 'on_notification')
112                     catch
113                         call lsp#log('s:on_stdout client request on_notification() error', v:exception, v:throwpoint)
114                     endtry
115                     unlet l:ctx['on_notifications'][l:response['id']]
116                 endif
117                 if has_key(l:ctx['requests'], l:response['id'])
118                     unlet l:ctx['requests'][l:response['id']]
119                 else
120                     call lsp#log('cannot find the request corresponding to response: ', l:response)
121                 endif
122             else
123                 " it is a notification
124                 if has_key(l:ctx['opts'], 'on_notification')
125                     try
126                         call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification')
127                     catch
128                         call lsp#log('s:on_stdout on_notification() error', v:exception, v:throwpoint)
129                     endtry
130                 endif
131             endif
132         endif
133
134         if empty(l:response_str)
135             " buffer is empty, wait for next message to arrive
136             return
137         endif
138     endwhile
139 endfunction
140
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, ':')
144         if len(l:kvp) == 2
145             if l:kvp[0] =~? '^Content-Length'
146                 return str2nr(l:kvp[1], 10)
147             endif
148         endif
149     endfor
150     return -1
151 endfunction
152
153 function! s:on_stderr(id, data, event) abort
154     let l:ctx = get(s:clients, a:id, {})
155     if empty(l:ctx)
156         return
157     endif
158     if has_key(l:ctx['opts'], 'on_stderr')
159         try
160             call l:ctx['opts']['on_stderr'](a:id, a:data, a:event)
161         catch
162             call lsp#log('s:on_stderr exception', v:exception, v:throwpoint)
163             echom v:exception
164         endtry
165     endif
166 endfunction
167
168 function! s:on_exit(id, status, event) abort
169     let l:ctx = get(s:clients, a:id, {})
170     if empty(l:ctx)
171         return
172     endif
173     if has_key(l:ctx['opts'], 'on_exit')
174         try
175             call l:ctx['opts']['on_exit'](a:id, a:status, a:event)
176         catch
177             call lsp#log('s:on_exit exception', v:exception, v:throwpoint)
178             echom v:exception
179         endtry
180     endif
181     call s:dispose_context(a:id)
182 endfunction
183
184 function! s:lsp_start(opts) abort
185     let l:opts = {
186         \ 'on_stdout': function('s:on_stdout'),
187         \ 'on_stderr': function('s:on_stderr'),
188         \ 'on_exit': function('s:on_exit'),
189         \ 'normalize': 'string'
190         \ }
191     if has_key(a:opts, 'env')
192         let l:opts.env = a:opts.env
193     endif
194
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)
199     else
200         return -1
201     endif
202
203     let l:ctx = s:create_context(l:client_id, a:opts)
204     let l:ctx['id'] = l:client_id
205
206     return l:client_id
207 endfunction
208
209 function! s:lsp_stop(id) abort
210     call lsp#utils#job#stop(a:id)
211 endfunction
212
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
219
220     let l:request = { 'jsonrpc': '2.0' }
221
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']
228         endif
229     endif
230
231     if has_key(a:opts, 'id')
232         let l:request['id'] = a:opts['id']
233     endif
234     if has_key(a:opts, 'method')
235         let l:request['method'] = a:opts['method']
236     endif
237     if has_key(a:opts, 'params')
238         let l:request['params'] = a:opts['params']
239     endif
240     if has_key(a:opts, 'result')
241         let l:request['result'] = a:opts['result']
242     endif
243     if has_key(a:opts, 'error')
244         let l:request['error'] = a:opts['error']
245     endif
246
247     let l:json = json_encode(l:request)
248     let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json
249
250     call lsp#utils#job#send(a:id, l:payload)
251
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'
258             endif
259         endif
260         return l:id
261     else
262         return 0
263     endif
264 endfunction
265
266 function! s:lsp_get_last_request_id(id) abort
267     return s:clients[a:id]['request_sequence']
268 endfunction
269
270 function! s:lsp_is_error(obj_or_response) abort
271     let l:vt = type(a:obj_or_response)
272     if l:vt == type('')
273         return len(a:obj_or_response) > 0
274     elseif l:vt == type({})
275         return has_key(a:obj_or_response, 'error')
276     endif
277     return 0
278 endfunction
279
280
281 function! s:is_server_instantiated_notification(notification) abort
282     return !has_key(a:notification, 'request')
283 endfunction
284
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)
293         endif
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 }
297         try
298             call l:ctx['opts']['on_notification'](l:ctx['id'], l:on_notification_data, 'on_notification')
299         catch
300             call lsp#log('s:native_notification_callback on_notification() error', v:exception, v:throwpoint)
301         endtry
302     endif
303 endfunction
304
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')
309         try
310             call l:ctx['opts']['on_stderr'](l:ctx['id'], a:response, 'stderr')
311         catch
312             call lsp#log('s:on_stderr exception', v:exception, v:throwpoint)
313             echom v:exception
314         endtry
315     endif
316 endfunction
317
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')
322         try
323             call l:ctx['opts']['on_exit'](l:ctx['id'], a:response, 'exit')
324         catch
325             call lsp#log('s:on_exit exception', v:exception, v:throwpoint)
326             echom v:exception
327         endtry
328     endif
329     call s:dispose_context(l:ctx['id'])
330 endfunction
331
332 " public apis {{{
333
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')
337             let l:cbctx = {}
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]),
342                 \ }
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
345             let s:jobidseq += 1
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
355             return l:jobid
356         elseif has_key(a:opts, 'tcp')
357             " add support for tcp
358             call lsp#log('tcp not supported when using native lsp client')
359             return -1
360         endif
361     endif
362     return s:lsp_start(a:opts)
363 endfunction
364
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'])
370     else
371         return s:lsp_stop(a:client_id)
372     endif
373 endfunction
374
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
379         let l:request = {}
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
383
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']
388         endif
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'
393             endif
394         endif
395         let l:ctx['request_sequence'] = l:request['id']
396         return l:request['id']
397     else
398         return s:lsp_send(a:client_id, a:opts, s:send_type_request)
399     endif
400 endfunction
401
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
407         try
408             call a:ctx['opts']['on_notification'](a:ctx['id'], l:on_notification_data, 'on_notification')
409         catch
410             call lsp#log('s:on_response_native client option on_notification() error', v:exception, v:throwpoint)
411         endtry
412     endif
413     if has_key(a:ctx['on_notifications'], a:request['id'])
414         " call lsp#client#send({ 'on_notification' }) second
415         try
416             call a:ctx['on_notifications'][a:request['id']](a:ctx['id'], l:on_notification_data, 'on_notification')
417         catch
418             call lsp#log('s:on_response_native client request on_notification() error', v:exception, v:throwpoint, a:request, a:response)
419         endtry
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']]
423         else
424             call lsp#log('cannot find the request corresponding to response: ', a:response)
425         endif
426     endif
427 endfunction
428
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
433         let l:request = {}
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)
437         return 0
438     else
439         return s:lsp_send(a:client_id, a:opts, s:send_type_notification)
440     endif
441 endfunction
442
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
447         let l:request = {}
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
451         try
452             call ch_sendexpr(l:ctx['channel'], l:request)
453         catch
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))
457         endtry
458         return 0
459     else
460         return s:lsp_send(a:client_id, a:opts, s:send_type_response)
461     endif
462 endfunction
463
464 function! lsp#client#get_last_request_id(client_id) abort
465     return s:lsp_get_last_request_id(a:client_id)
466 endfunction
467
468 function! lsp#client#is_error(obj_or_response) abort
469     return s:lsp_is_error(a:obj_or_response)
470 endfunction
471
472 function! lsp#client#error_message(obj_or_response) abort
473     try
474         return a:obj_or_response['error']['data']['message']
475     catch
476     endtry
477     try
478         return a:obj_or_response['error']['message']
479     catch
480     endtry
481     return string(a:obj_or_response)
482 endfunction
483
484 function! lsp#client#is_server_instantiated_notification(notification) abort
485     return s:is_server_instantiated_notification(a:notification)
486 endfunction
487
488 " }}}
489
490 let &cpoptions = s:save_cpo
491 unlet s:save_cpo
492 " vim sw=4 ts=4 et