]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/lsp.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/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / lsp.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Language Server Protocol client code
3
4 " A Dictionary for tracking connections.
5 let s:connections = get(s:, 'connections', {})
6 let g:ale_lsp_next_message_id = 1
7
8 " Given an id, which can be an executable or address, a project path,
9 " and a language string or (bufnr) -> string function
10 " create a new connection if needed. Return a unique ID for the connection.
11 function! ale#lsp#Register(executable_or_address, project, language, init_options) abort
12     let l:conn_id = a:executable_or_address . ':' . a:project
13
14     if !has_key(s:connections, l:conn_id)
15         " is_tsserver: 1 if the connection is for tsserver.
16         " data: The message data received so far.
17         " root: The project root.
18         " open_documents: A Dictionary mapping buffers to b:changedtick, keeping
19         "   track of when documents were opened, and when we last changed them.
20         " initialized: 0 if the connection is ready, 1 otherwise.
21         " init_request_id: The ID for the init request.
22         " init_options: Options to send to the server.
23         " config: Configuration settings to send to the server.
24         " callback_list: A list of callbacks for handling LSP responses.
25         " capabilities_queue: The list of callbacks to call with capabilities.
26         " capabilities: Features the server supports.
27         let s:connections[l:conn_id] = {
28         \   'id': l:conn_id,
29         \   'is_tsserver': 0,
30         \   'data': '',
31         \   'root': a:project,
32         \   'language': a:language,
33         \   'open_documents': {},
34         \   'initialized': 0,
35         \   'init_request_id': 0,
36         \   'init_options': a:init_options,
37         \   'config': {},
38         \   'callback_list': [],
39         \   'init_queue': [],
40         \   'capabilities': {
41         \       'hover': 0,
42         \       'rename': 0,
43         \       'filerename': 0,
44         \       'references': 0,
45         \       'completion': 0,
46         \       'completion_trigger_characters': [],
47         \       'definition': 0,
48         \       'typeDefinition': 0,
49         \       'implementation': 0,
50         \       'pull_model': 0,
51         \       'symbol_search': 0,
52         \       'code_actions': 0,
53         \       'did_save': 0,
54         \       'includeText': 0,
55         \   },
56         \}
57     endif
58
59     return l:conn_id
60 endfunction
61
62 " Remove an LSP connection with a given ID. This is only for tests.
63 function! ale#lsp#RemoveConnectionWithID(id) abort
64     if has_key(s:connections, a:id)
65         call remove(s:connections, a:id)
66     endif
67 endfunction
68
69 function! ale#lsp#ResetConnections() abort
70     let s:connections = {}
71 endfunction
72
73 " Used only in tests.
74 function! ale#lsp#GetConnections() abort
75     " This command will throw from the sandbox.
76     let &l:equalprg=&l:equalprg
77
78     return s:connections
79 endfunction
80
81 " This is only needed for tests
82 function! ale#lsp#MarkDocumentAsOpen(id, buffer) abort
83     let l:conn = get(s:connections, a:id, {})
84
85     if !empty(l:conn)
86         let l:conn.open_documents[a:buffer] = -1
87     endif
88 endfunction
89
90 function! ale#lsp#GetNextMessageID() abort
91     " Use the current ID
92     let l:id = g:ale_lsp_next_message_id
93
94     " Increment the ID variable.
95     let g:ale_lsp_next_message_id += 1
96
97     " When the ID overflows, reset it to 1. By the time we hit the initial ID
98     " again, the messages will be long gone.
99     if g:ale_lsp_next_message_id < 1
100         let g:ale_lsp_next_message_id = 1
101     endif
102
103     return l:id
104 endfunction
105
106 " TypeScript messages use a different format.
107 function! s:CreateTSServerMessageData(message) abort
108     let l:is_notification = a:message[0]
109
110     let l:obj = {
111     \   'seq': v:null,
112     \   'type': 'request',
113     \   'command': a:message[1][3:],
114     \}
115
116     if !l:is_notification
117         let l:obj.seq = ale#lsp#GetNextMessageID()
118     endif
119
120     if len(a:message) > 2
121         let l:obj.arguments = a:message[2]
122     endif
123
124     let l:data = json_encode(l:obj) . "\n"
125
126     return [l:is_notification ? 0 : l:obj.seq, l:data]
127 endfunction
128
129 " Given a List of one or two items, [method_name] or [method_name, params],
130 " return a List containing [message_id, message_data]
131 function! ale#lsp#CreateMessageData(message) abort
132     if a:message[1][:2] is# 'ts@'
133         return s:CreateTSServerMessageData(a:message)
134     endif
135
136     let l:is_notification = a:message[0]
137
138     let l:obj = {
139     \   'method': a:message[1],
140     \   'jsonrpc': '2.0',
141     \}
142
143     if !l:is_notification
144         let l:obj.id = ale#lsp#GetNextMessageID()
145     endif
146
147     if len(a:message) > 2
148         let l:obj.params = a:message[2]
149     endif
150
151     let l:body = json_encode(l:obj)
152     let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body
153
154     return [l:is_notification ? 0 : l:obj.id, l:data]
155 endfunction
156
157 function! ale#lsp#ReadMessageData(data) abort
158     let l:response_list = []
159     let l:remainder = a:data
160
161     while 1
162         " Look for the end of the HTTP headers
163         let l:body_start_index = matchend(l:remainder, "\r\n\r\n")
164
165         if l:body_start_index < 0
166             " No header end was found yet.
167             break
168         endif
169
170         " Parse the Content-Length header.
171         let l:header_data = l:remainder[:l:body_start_index - 4]
172         let l:length_match = matchlist(
173         \   l:header_data,
174         \   '\vContent-Length: *(\d+)'
175         \)
176
177         if empty(l:length_match)
178             throw "Invalid JSON-RPC header:\n" . l:header_data
179         endif
180
181         " Split the body and the remainder of the text.
182         let l:remainder_start_index = l:body_start_index + str2nr(l:length_match[1])
183
184         if len(l:remainder) < l:remainder_start_index
185             " We don't have enough data yet.
186             break
187         endif
188
189         let l:body = l:remainder[l:body_start_index : l:remainder_start_index - 1]
190         let l:remainder = l:remainder[l:remainder_start_index :]
191
192         " Parse the JSON object and add it to the list.
193         call add(l:response_list, json_decode(l:body))
194     endwhile
195
196     return [l:remainder, l:response_list]
197 endfunction
198
199 " Update capabilities from the server, so we know which features the server
200 " supports.
201 function! ale#lsp#UpdateCapabilities(conn_id, capabilities) abort
202     let l:conn = get(s:connections, a:conn_id, {})
203
204     if empty(l:conn)
205         return
206     endif
207
208     if type(a:capabilities) isnot v:t_dict
209         return
210     endif
211
212     if get(a:capabilities, 'hoverProvider') is v:true
213         let l:conn.capabilities.hover = 1
214     endif
215
216     if type(get(a:capabilities, 'hoverProvider')) is v:t_dict
217         let l:conn.capabilities.hover = 1
218     endif
219
220     if get(a:capabilities, 'referencesProvider') is v:true
221         let l:conn.capabilities.references = 1
222     endif
223
224     if type(get(a:capabilities, 'referencesProvider')) is v:t_dict
225         let l:conn.capabilities.references = 1
226     endif
227
228     if get(a:capabilities, 'renameProvider') is v:true
229         let l:conn.capabilities.rename = 1
230     endif
231
232     if type(get(a:capabilities, 'renameProvider')) is v:t_dict
233         let l:conn.capabilities.rename = 1
234     endif
235
236     if get(a:capabilities, 'codeActionProvider') is v:true
237         let l:conn.capabilities.code_actions = 1
238     endif
239
240     if type(get(a:capabilities, 'codeActionProvider')) is v:t_dict
241         let l:conn.capabilities.code_actions = 1
242     endif
243
244     if !empty(get(a:capabilities, 'completionProvider'))
245         let l:conn.capabilities.completion = 1
246     endif
247
248     if type(get(a:capabilities, 'completionProvider')) is v:t_dict
249         let l:chars = get(a:capabilities.completionProvider, 'triggerCharacters')
250
251         if type(l:chars) is v:t_list
252             let l:conn.capabilities.completion_trigger_characters = l:chars
253         endif
254     endif
255
256     if get(a:capabilities, 'definitionProvider') is v:true
257         let l:conn.capabilities.definition = 1
258     endif
259
260     if type(get(a:capabilities, 'definitionProvider')) is v:t_dict
261         let l:conn.capabilities.definition = 1
262     endif
263
264     if get(a:capabilities, 'typeDefinitionProvider') is v:true
265         let l:conn.capabilities.typeDefinition = 1
266     endif
267
268     if type(get(a:capabilities, 'typeDefinitionProvider')) is v:t_dict
269         let l:conn.capabilities.typeDefinition = 1
270     endif
271
272     if get(a:capabilities, 'implementationProvider') is v:true
273         let l:conn.capabilities.implementation = 1
274     endif
275
276     if type(get(a:capabilities, 'implementationProvider')) is v:t_dict
277         let l:conn.capabilities.implementation = 1
278     endif
279
280     " Check if the language server supports pull model diagnostics.
281     if type(get(a:capabilities, 'diagnosticProvider')) is v:t_dict
282         if type(get(a:capabilities.diagnosticProvider, 'interFileDependencies')) is v:t_bool
283             let l:conn.capabilities.pull_model = 1
284         endif
285     endif
286
287     if get(a:capabilities, 'workspaceSymbolProvider') is v:true
288         let l:conn.capabilities.symbol_search = 1
289     endif
290
291     if type(get(a:capabilities, 'workspaceSymbolProvider')) is v:t_dict
292         let l:conn.capabilities.symbol_search = 1
293     endif
294
295     if type(get(a:capabilities, 'textDocumentSync')) is v:t_dict
296         let l:syncOptions = get(a:capabilities, 'textDocumentSync')
297
298         if get(l:syncOptions, 'save') is v:true
299             let l:conn.capabilities.did_save = 1
300         endif
301
302         if type(get(l:syncOptions, 'save')) is v:t_dict
303             let l:conn.capabilities.did_save = 1
304
305             let l:saveOptions = get(l:syncOptions, 'save')
306
307             if get(l:saveOptions, 'includeText') is v:true
308                 let l:conn.capabilities.includeText = 1
309             endif
310         endif
311     endif
312 endfunction
313
314 " Update a connection's configuration dictionary and notify LSP servers
315 " of any changes since the last update. Returns 1 if a configuration
316 " update was sent; otherwise 0 will be returned.
317 function! ale#lsp#UpdateConfig(conn_id, buffer, config) abort
318     let l:conn = get(s:connections, a:conn_id, {})
319
320     if empty(l:conn) || a:config ==# l:conn.config " no-custom-checks
321         return 0
322     endif
323
324     let l:conn.config = a:config
325     let l:message = ale#lsp#message#DidChangeConfiguration(a:buffer, a:config)
326
327     call ale#lsp#Send(a:conn_id, l:message)
328
329     return 1
330 endfunction
331
332 function! ale#lsp#CallInitCallbacks(conn_id) abort
333     let l:conn = get(s:connections, a:conn_id, {})
334
335     if !empty(l:conn)
336         " Ensure the connection is marked as initialized.
337         " For integration with Neovim's LSP tooling this ensures immediately
338         " call OnInit functions in Vim after the `on_init` callback is called.
339         let l:conn.initialized = 1
340
341         " Call capabilities callbacks queued for the project.
342         for l:Callback in l:conn.init_queue
343             call l:Callback()
344         endfor
345
346         let l:conn.init_queue = []
347     endif
348 endfunction
349
350 function! ale#lsp#HandleInitResponse(conn, response) abort
351     if get(a:response, 'method', '') is# 'initialize'
352         let a:conn.initialized = 1
353     elseif type(get(a:response, 'result')) is v:t_dict
354     \&& has_key(a:response.result, 'capabilities')
355         call ale#lsp#UpdateCapabilities(a:conn.id, a:response.result.capabilities)
356
357         let a:conn.initialized = 1
358     endif
359
360     if !a:conn.initialized
361         return
362     endif
363
364     " The initialized message must be sent before everything else.
365     call ale#lsp#Send(a:conn.id, ale#lsp#message#Initialized())
366
367     call ale#lsp#CallInitCallbacks(a:conn.id)
368 endfunction
369
370 function! ale#lsp#HandleMessage(conn_id, message) abort
371     let l:conn = get(s:connections, a:conn_id, {})
372
373     if empty(l:conn)
374         return
375     endif
376
377     if type(a:message) isnot v:t_string
378         " Ignore messages that aren't strings.
379         return
380     endif
381
382     let l:conn.data .= a:message
383
384     " Parse the objects now if we can, and keep the remaining text.
385     let [l:conn.data, l:response_list] = ale#lsp#ReadMessageData(l:conn.data)
386
387     " Look for initialize responses first.
388     if !l:conn.initialized
389         for l:response in l:response_list
390             call ale#lsp#HandleInitResponse(l:conn, l:response)
391         endfor
392     endif
393
394     " If the connection is marked as initialized, call the callbacks with the
395     " responses.
396     if l:conn.initialized
397         for l:response in l:response_list
398             " Call all of the registered handlers with the response.
399             for l:Callback in l:conn.callback_list
400                 call ale#util#GetFunction(l:Callback)(a:conn_id, l:response)
401             endfor
402         endfor
403     endif
404 endfunction
405
406 " Handle a JSON response from a language server.
407 " This is called from Lua for integration with Neovim's LSP API.
408 function! ale#lsp#HandleResponse(conn_id, response) abort
409     let l:conn = get(s:connections, a:conn_id, {})
410
411     if empty(l:conn)
412         return
413     endif
414
415     for l:Callback in l:conn.callback_list
416         call ale#util#GetFunction(l:Callback)(a:conn_id, a:response)
417     endfor
418 endfunction
419
420 " Given a connection ID, mark it as a tsserver connection, so it will be
421 " handled that way.
422 function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort
423     let l:conn = s:connections[a:conn_id]
424     let l:conn.is_tsserver = 1
425     let l:conn.initialized = 1
426     " Set capabilities which are supported by tsserver.
427     let l:conn.capabilities.hover = 1
428     let l:conn.capabilities.references = 1
429     let l:conn.capabilities.completion = 1
430     let l:conn.capabilities.completion_trigger_characters = ['.']
431     let l:conn.capabilities.definition = 1
432     let l:conn.capabilities.typeDefinition = 1
433     let l:conn.capabilities.implementation = 1
434     let l:conn.capabilities.symbol_search = 1
435     let l:conn.capabilities.rename = 1
436     let l:conn.capabilities.filerename = 1
437     let l:conn.capabilities.code_actions = 1
438 endfunction
439
440 function! s:SendInitMessage(conn) abort
441     let [l:init_id, l:init_data] = ale#lsp#CreateMessageData(
442     \   ale#lsp#message#Initialize(
443     \       a:conn.root,
444     \       a:conn.init_options,
445     \       {
446     \           'workspace': {
447     \               'applyEdit': v:false,
448     \               'didChangeConfiguration': {
449     \                   'dynamicRegistration': v:false,
450     \               },
451     \               'symbol': {
452     \                   'dynamicRegistration': v:false,
453     \               },
454     \               'workspaceFolders': v:false,
455     \               'configuration': v:false,
456     \           },
457     \           'textDocument': {
458     \               'synchronization': {
459     \                   'dynamicRegistration': v:false,
460     \                   'willSave': v:false,
461     \                   'willSaveWaitUntil': v:false,
462     \                   'didSave': v:true,
463     \               },
464     \               'completion': {
465     \                   'dynamicRegistration': v:false,
466     \                   'completionItem': {
467     \                       'snippetSupport': v:false,
468     \                       'commitCharactersSupport': v:false,
469     \                       'documentationFormat': ['plaintext', 'markdown'],
470     \                       'deprecatedSupport': v:false,
471     \                       'preselectSupport': v:false,
472     \                   },
473     \                   'contextSupport': v:false,
474     \               },
475     \               'hover': {
476     \                   'dynamicRegistration': v:false,
477     \                   'contentFormat': ['plaintext', 'markdown'],
478     \               },
479     \               'references': {
480     \                   'dynamicRegistration': v:false,
481     \               },
482     \               'documentSymbol': {
483     \                   'dynamicRegistration': v:false,
484     \                   'hierarchicalDocumentSymbolSupport': v:false,
485     \               },
486     \               'definition': {
487     \                   'dynamicRegistration': v:false,
488     \                   'linkSupport': v:false,
489     \               },
490     \               'typeDefinition': {
491     \                   'dynamicRegistration': v:false,
492     \               },
493     \               'implementation': {
494     \                   'dynamicRegistration': v:false,
495     \                   'linkSupport': v:false,
496     \               },
497     \               'diagnostic': {
498     \                   'dynamicRegistration': v:true,
499     \                   'relatedDocumentSupport': v:true,
500     \               },
501     \               'publishDiagnostics': {
502     \                   'relatedInformation': v:true,
503     \               },
504     \               'codeAction': {
505     \                   'dynamicRegistration': v:false,
506     \                   'codeActionLiteralSupport': {
507     \                        'codeActionKind': {
508     \                            'valueSet': []
509     \                        }
510     \                    }
511     \               },
512     \               'rename': {
513     \                   'dynamicRegistration': v:false,
514     \               },
515     \           },
516     \       },
517     \   ),
518     \)
519     let a:conn.init_request_id = l:init_id
520     call s:SendMessageData(a:conn, l:init_data)
521 endfunction
522
523 " Start a program for LSP servers.
524 "
525 " 1 will be returned if the program is running, or 0 if the program could
526 " not be started.
527 function! ale#lsp#StartProgram(conn_id, executable, command) abort
528     let l:conn = s:connections[a:conn_id]
529     let l:started = 0
530
531     if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver
532         " For Windows from 'cmd /s/c "foo bar"' we need 'foo bar'
533         let l:lsp_cmd = has('win32') && type(a:command) is v:t_string
534         \   ? ['cmd', '/s/c/', a:command[10:-2]]
535         \   : a:command
536
537         " Always call lsp.start, which will either create or re-use a
538         " connection. We'll set `attach` to `false` so we can later use
539         " our OpenDocument function to attach the buffer separately.
540         let l:client_id = luaeval('require("ale.lsp").start(_A)', {
541         \   'name': a:conn_id,
542         \   'cmd': l:lsp_cmd,
543         \   'root_dir': l:conn.root,
544         \   'init_options': l:conn.init_options,
545         \})
546
547         if l:client_id > 0
548             let l:conn.client_id = l:client_id
549         endif
550
551         return l:client_id > 0
552     endif
553
554     if !has_key(l:conn, 'job_id') || !ale#job#HasOpenChannel(l:conn.job_id)
555         let l:options = {
556         \   'mode': 'raw',
557         \   'out_cb': {_, message -> ale#lsp#HandleMessage(a:conn_id, message)},
558         \   'exit_cb': { -> ale#lsp#Stop(a:conn_id) },
559         \}
560
561         if has('win32')
562             let l:job_id = ale#job#StartWithCmd(a:command, l:options)
563         else
564             let l:job_id = ale#job#Start(a:command, l:options)
565         endif
566
567         let l:started = 1
568     else
569         let l:job_id = l:conn.job_id
570     endif
571
572     if l:job_id > 0
573         let l:conn.job_id = l:job_id
574     endif
575
576     if l:started && !l:conn.is_tsserver
577         let l:conn.initialized = 0
578         call s:SendInitMessage(l:conn)
579     endif
580
581     return l:job_id > 0
582 endfunction
583
584 " Split an address into [host, port].
585 " The port will either be a number or v:null.
586 function! ale#lsp#SplitAddress(address) abort
587     let l:port_match = matchlist(a:address, '\v:(\d+)$')
588
589     if !empty(l:port_match)
590         let l:host = a:address[:-len(l:port_match[1]) - 2]
591         let l:port = l:port_match[1] + 0
592
593         return [l:host, l:port ? l:port : v:null]
594     endif
595
596     return [a:address, v:null]
597 endfunction
598
599 " Connect to an LSP server via TCP.
600 "
601 " 1 will be returned if the connection is running, or 0 if the connection could
602 " not be opened.
603 function! ale#lsp#ConnectToAddress(conn_id, address) abort
604     let l:conn = s:connections[a:conn_id]
605     let l:started = 0
606
607     if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver
608         let [l:host, l:port] = ale#lsp#SplitAddress(a:address)
609
610         let l:client_id = luaeval('require("ale.lsp").start(_A)', {
611         \   'name': a:conn_id,
612         \   'host': l:host,
613         \   'port': l:port,
614         \   'root_dir': l:conn.root,
615         \   'init_options': l:conn.init_options,
616         \})
617
618         if l:client_id > 0
619             let l:conn.client_id = l:client_id
620         endif
621
622         return l:client_id > 0
623     elseif !has_key(l:conn, 'channel_id') || !ale#socket#IsOpen(l:conn.channel_id)
624         let l:channel_id = ale#socket#Open(a:address, {
625         \   'callback': {_, mess -> ale#lsp#HandleMessage(a:conn_id, mess)},
626         \})
627
628         let l:started = 1
629     else
630         let l:channel_id = l:conn.channel_id
631     endif
632
633     if l:channel_id >= 0
634         let l:conn.channel_id = l:channel_id
635     endif
636
637     if l:started
638         call s:SendInitMessage(l:conn)
639     endif
640
641     return l:channel_id >= 0
642 endfunction
643
644 " Given a connection ID and a callback, register that callback for handling
645 " messages if the connection exists.
646 function! ale#lsp#RegisterCallback(conn_id, callback) abort
647     let l:conn = get(s:connections, a:conn_id, {})
648
649     if !empty(l:conn)
650         " Add the callback to the List if it's not there already.
651         call uniq(sort(add(l:conn.callback_list, a:callback)))
652     endif
653 endfunction
654
655 " Stop a single LSP connection.
656 function! ale#lsp#Stop(conn_id) abort
657     if has_key(s:connections, a:conn_id)
658         let l:conn = remove(s:connections, a:conn_id)
659
660         if has_key(l:conn, 'channel_id')
661             call ale#socket#Close(l:conn.channel_id)
662         elseif has_key(l:conn, 'job_id')
663             call ale#job#Stop(l:conn.job_id)
664         endif
665     endif
666 endfunction
667
668 function! ale#lsp#CloseDocument(conn_id) abort
669 endfunction
670
671 " Stop all LSP connections, closing all jobs and channels, and removing any
672 " queued messages.
673 function! ale#lsp#StopAll() abort
674     for l:conn_id in keys(s:connections)
675         call ale#lsp#Stop(l:conn_id)
676     endfor
677 endfunction
678
679 function! s:SendMessageData(conn, data) abort
680     if has_key(a:conn, 'job_id')
681         call ale#job#SendRaw(a:conn.job_id, a:data)
682     elseif has_key(a:conn, 'channel_id') && ale#socket#IsOpen(a:conn.channel_id)
683         " Send the message to the server
684         call ale#socket#Send(a:conn.channel_id, a:data)
685     else
686         return 0
687     endif
688
689     return 1
690 endfunction
691
692 " Send a message to an LSP server.
693 " Notifications do not need to be handled.
694 "
695 " Returns -1 when a message is sent, but no response is expected
696 "          0 when the message is not sent and
697 "          >= 1 with the message ID when a response is expected.
698 function! ale#lsp#Send(conn_id, message) abort
699     let l:conn = get(s:connections, a:conn_id, {})
700
701     if empty(l:conn)
702         return 0
703     endif
704
705     if !l:conn.initialized
706         throw 'LSP server not initialized yet!'
707     endif
708
709     if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver
710         return luaeval('require("ale.lsp").send_message(_A)', {
711         \   'client_id': l:conn.client_id,
712         \   'is_notification': a:message[0] == 1 ? v:true : v:false,
713         \   'method': a:message[1],
714         \   'params': get(a:message, 2, v:null)
715         \})
716     endif
717
718     let [l:id, l:data] = ale#lsp#CreateMessageData(a:message)
719     call s:SendMessageData(l:conn, l:data)
720
721     return l:id == 0 ? -1 : l:id
722 endfunction
723
724 function! ale#lsp#GetLanguage(conn_id, buffer) abort
725     let l:conn = get(s:connections, a:conn_id, {})
726     let l:Language = get(l:conn, 'language')
727
728     if empty(l:Language)
729         return getbufvar(a:buffer, '&filetype')
730     endif
731
732     return type(l:Language) is v:t_func ? l:Language(a:buffer) : l:Language
733 endfunction
734
735 " Notify LSP servers or tsserver if a document is opened, if needed.
736 " If a document is opened, 1 will be returned, otherwise 0 will be returned.
737 function! ale#lsp#OpenDocument(conn_id, buffer) abort
738     let l:conn = get(s:connections, a:conn_id, {})
739     let l:opened = 0
740
741     if !empty(l:conn) && !has_key(l:conn.open_documents, a:buffer)
742         if l:conn.is_tsserver
743             let l:message = ale#lsp#tsserver_message#Open(a:buffer)
744             call ale#lsp#Send(a:conn_id, l:message)
745         elseif g:ale_use_neovim_lsp_api
746             call luaeval('require("ale.lsp").buf_attach(_A)', {
747             \    'bufnr': a:buffer,
748             \    'client_id': l:conn.client_id,
749             \})
750         else
751             let l:language_id = ale#lsp#GetLanguage(a:conn_id, a:buffer)
752             let l:message = ale#lsp#message#DidOpen(a:buffer, l:language_id)
753             call ale#lsp#Send(a:conn_id, l:message)
754         endif
755
756         let l:conn.open_documents[a:buffer] = getbufvar(a:buffer, 'changedtick')
757         let l:opened = 1
758     endif
759
760     return l:opened
761 endfunction
762
763 " Notify LSP servers or tsserver that a document is closed, if opened before.
764 " If a document is closed, 1 will be returned, otherwise 0 will be returned.
765 "
766 " Only the buffer number is required here. A message will be sent to every
767 " language server that was notified previously of the document being opened.
768 function! ale#lsp#CloseDocument(buffer) abort
769     let l:closed = 0
770
771     " The connection keys are sorted so the messages are easier to test, and
772     " so messages are sent in a consistent order.
773     for l:conn_id in sort(keys(s:connections))
774         let l:conn = s:connections[l:conn_id]
775
776         if l:conn.initialized && has_key(l:conn.open_documents, a:buffer)
777             if l:conn.is_tsserver
778                 let l:message = ale#lsp#tsserver_message#Close(a:buffer)
779                 call ale#lsp#Send(l:conn_id, l:message)
780             elseif g:ale_use_neovim_lsp_api
781                 call luaeval('require("ale.lsp").buf_detach(_A)', {
782                 \    'bufnr': a:buffer,
783                 \    'client_id': l:conn.client_id,
784                 \})
785             else
786                 let l:message = ale#lsp#message#DidClose(a:buffer)
787                 call ale#lsp#Send(l:conn_id, l:message)
788             endif
789
790             call remove(l:conn.open_documents, a:buffer)
791             let l:closed = 1
792         endif
793     endfor
794
795     return l:closed
796 endfunction
797
798 " Notify LSP servers or tsserver that a document has changed, if needed.
799 " If a notification is sent, 1 will be returned, otherwise 0 will be returned.
800 function! ale#lsp#NotifyForChanges(conn_id, buffer) abort
801     let l:conn = get(s:connections, a:conn_id, {})
802     let l:notified = 0
803
804     if !empty(l:conn) && has_key(l:conn.open_documents, a:buffer)
805         let l:new_tick = getbufvar(a:buffer, 'changedtick')
806
807         if l:conn.open_documents[a:buffer] < l:new_tick
808             if l:conn.is_tsserver
809                 let l:message = ale#lsp#tsserver_message#Change(a:buffer)
810             else
811                 let l:message = ale#lsp#message#DidChange(a:buffer)
812             endif
813
814             call ale#lsp#Send(a:conn_id, l:message)
815             let l:conn.open_documents[a:buffer] = l:new_tick
816             let l:notified = 1
817         endif
818     endif
819
820     return l:notified
821 endfunction
822
823 " Wait for an LSP server to be initialized.
824 function! ale#lsp#OnInit(conn_id, Callback) abort
825     let l:conn = get(s:connections, a:conn_id, {})
826
827     if empty(l:conn)
828         return
829     endif
830
831     if l:conn.initialized
832         call a:Callback()
833     else
834         call add(l:conn.init_queue, a:Callback)
835     endif
836 endfunction
837
838 " Check if an LSP has a given capability.
839 function! ale#lsp#HasCapability(conn_id, capability) abort
840     let l:conn = get(s:connections, a:conn_id, {})
841
842     if empty(l:conn)
843         return 0
844     endif
845
846     if type(get(l:conn.capabilities, a:capability, v:null)) isnot v:t_number
847         throw 'Invalid capability ' . a:capability
848     endif
849
850     return l:conn.capabilities[a:capability]
851 endfunction