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 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Language Server Protocol client code
4 " A Dictionary for tracking connections.
5 let s:connections = get(s:, 'connections', {})
6 let g:ale_lsp_next_message_id = 1
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
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] = {
32 \ 'language': a:language,
33 \ 'open_documents': {},
35 \ 'init_request_id': 0,
36 \ 'init_options': a:init_options,
38 \ 'callback_list': [],
46 \ 'completion_trigger_characters': [],
48 \ 'typeDefinition': 0,
49 \ 'implementation': 0,
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)
69 function! ale#lsp#ResetConnections() abort
70 let s:connections = {}
74 function! ale#lsp#GetConnections() abort
75 " This command will throw from the sandbox.
76 let &l:equalprg=&l:equalprg
81 " This is only needed for tests
82 function! ale#lsp#MarkDocumentAsOpen(id, buffer) abort
83 let l:conn = get(s:connections, a:id, {})
86 let l:conn.open_documents[a:buffer] = -1
90 function! ale#lsp#GetNextMessageID() abort
92 let l:id = g:ale_lsp_next_message_id
94 " Increment the ID variable.
95 let g:ale_lsp_next_message_id += 1
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
106 " TypeScript messages use a different format.
107 function! s:CreateTSServerMessageData(message) abort
108 let l:is_notification = a:message[0]
113 \ 'command': a:message[1][3:],
116 if !l:is_notification
117 let l:obj.seq = ale#lsp#GetNextMessageID()
120 if len(a:message) > 2
121 let l:obj.arguments = a:message[2]
124 let l:data = json_encode(l:obj) . "\n"
126 return [l:is_notification ? 0 : l:obj.seq, l:data]
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)
136 let l:is_notification = a:message[0]
139 \ 'method': a:message[1],
143 if !l:is_notification
144 let l:obj.id = ale#lsp#GetNextMessageID()
147 if len(a:message) > 2
148 let l:obj.params = a:message[2]
151 let l:body = json_encode(l:obj)
152 let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body
154 return [l:is_notification ? 0 : l:obj.id, l:data]
157 function! ale#lsp#ReadMessageData(data) abort
158 let l:response_list = []
159 let l:remainder = a:data
162 " Look for the end of the HTTP headers
163 let l:body_start_index = matchend(l:remainder, "\r\n\r\n")
165 if l:body_start_index < 0
166 " No header end was found yet.
170 " Parse the Content-Length header.
171 let l:header_data = l:remainder[:l:body_start_index - 4]
172 let l:length_match = matchlist(
174 \ '\vContent-Length: *(\d+)'
177 if empty(l:length_match)
178 throw "Invalid JSON-RPC header:\n" . l:header_data
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])
184 if len(l:remainder) < l:remainder_start_index
185 " We don't have enough data yet.
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 :]
192 " Parse the JSON object and add it to the list.
193 call add(l:response_list, json_decode(l:body))
196 return [l:remainder, l:response_list]
199 " Update capabilities from the server, so we know which features the server
201 function! ale#lsp#UpdateCapabilities(conn_id, capabilities) abort
202 let l:conn = get(s:connections, a:conn_id, {})
208 if type(a:capabilities) isnot v:t_dict
212 if get(a:capabilities, 'hoverProvider') is v:true
213 let l:conn.capabilities.hover = 1
216 if type(get(a:capabilities, 'hoverProvider')) is v:t_dict
217 let l:conn.capabilities.hover = 1
220 if get(a:capabilities, 'referencesProvider') is v:true
221 let l:conn.capabilities.references = 1
224 if type(get(a:capabilities, 'referencesProvider')) is v:t_dict
225 let l:conn.capabilities.references = 1
228 if get(a:capabilities, 'renameProvider') is v:true
229 let l:conn.capabilities.rename = 1
232 if type(get(a:capabilities, 'renameProvider')) is v:t_dict
233 let l:conn.capabilities.rename = 1
236 if get(a:capabilities, 'codeActionProvider') is v:true
237 let l:conn.capabilities.code_actions = 1
240 if type(get(a:capabilities, 'codeActionProvider')) is v:t_dict
241 let l:conn.capabilities.code_actions = 1
244 if !empty(get(a:capabilities, 'completionProvider'))
245 let l:conn.capabilities.completion = 1
248 if type(get(a:capabilities, 'completionProvider')) is v:t_dict
249 let l:chars = get(a:capabilities.completionProvider, 'triggerCharacters')
251 if type(l:chars) is v:t_list
252 let l:conn.capabilities.completion_trigger_characters = l:chars
256 if get(a:capabilities, 'definitionProvider') is v:true
257 let l:conn.capabilities.definition = 1
260 if type(get(a:capabilities, 'definitionProvider')) is v:t_dict
261 let l:conn.capabilities.definition = 1
264 if get(a:capabilities, 'typeDefinitionProvider') is v:true
265 let l:conn.capabilities.typeDefinition = 1
268 if type(get(a:capabilities, 'typeDefinitionProvider')) is v:t_dict
269 let l:conn.capabilities.typeDefinition = 1
272 if get(a:capabilities, 'implementationProvider') is v:true
273 let l:conn.capabilities.implementation = 1
276 if type(get(a:capabilities, 'implementationProvider')) is v:t_dict
277 let l:conn.capabilities.implementation = 1
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
287 if get(a:capabilities, 'workspaceSymbolProvider') is v:true
288 let l:conn.capabilities.symbol_search = 1
291 if type(get(a:capabilities, 'workspaceSymbolProvider')) is v:t_dict
292 let l:conn.capabilities.symbol_search = 1
295 if type(get(a:capabilities, 'textDocumentSync')) is v:t_dict
296 let l:syncOptions = get(a:capabilities, 'textDocumentSync')
298 if get(l:syncOptions, 'save') is v:true
299 let l:conn.capabilities.did_save = 1
302 if type(get(l:syncOptions, 'save')) is v:t_dict
303 let l:conn.capabilities.did_save = 1
305 let l:saveOptions = get(l:syncOptions, 'save')
307 if get(l:saveOptions, 'includeText') is v:true
308 let l:conn.capabilities.includeText = 1
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, {})
320 if empty(l:conn) || a:config ==# l:conn.config " no-custom-checks
324 let l:conn.config = a:config
325 let l:message = ale#lsp#message#DidChangeConfiguration(a:buffer, a:config)
327 call ale#lsp#Send(a:conn_id, l:message)
332 function! ale#lsp#CallInitCallbacks(conn_id) abort
333 let l:conn = get(s:connections, a:conn_id, {})
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
341 " Call capabilities callbacks queued for the project.
342 for l:Callback in l:conn.init_queue
346 let l:conn.init_queue = []
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)
357 let a:conn.initialized = 1
360 if !a:conn.initialized
364 " The initialized message must be sent before everything else.
365 call ale#lsp#Send(a:conn.id, ale#lsp#message#Initialized())
367 call ale#lsp#CallInitCallbacks(a:conn.id)
370 function! ale#lsp#HandleMessage(conn_id, message) abort
371 let l:conn = get(s:connections, a:conn_id, {})
377 if type(a:message) isnot v:t_string
378 " Ignore messages that aren't strings.
382 let l:conn.data .= a:message
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)
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)
394 " If the connection is marked as initialized, call the callbacks with the
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)
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, {})
415 for l:Callback in l:conn.callback_list
416 call ale#util#GetFunction(l:Callback)(a:conn_id, a:response)
420 " Given a connection ID, mark it as a tsserver connection, so it will be
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
440 function! s:SendInitMessage(conn) abort
441 let [l:init_id, l:init_data] = ale#lsp#CreateMessageData(
442 \ ale#lsp#message#Initialize(
444 \ a:conn.init_options,
447 \ 'applyEdit': v:false,
448 \ 'didChangeConfiguration': {
449 \ 'dynamicRegistration': v:false,
452 \ 'dynamicRegistration': v:false,
454 \ 'workspaceFolders': v:false,
455 \ 'configuration': v:false,
458 \ 'synchronization': {
459 \ 'dynamicRegistration': v:false,
460 \ 'willSave': v:false,
461 \ 'willSaveWaitUntil': v:false,
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,
473 \ 'contextSupport': v:false,
476 \ 'dynamicRegistration': v:false,
477 \ 'contentFormat': ['plaintext', 'markdown'],
480 \ 'dynamicRegistration': v:false,
482 \ 'documentSymbol': {
483 \ 'dynamicRegistration': v:false,
484 \ 'hierarchicalDocumentSymbolSupport': v:false,
487 \ 'dynamicRegistration': v:false,
488 \ 'linkSupport': v:false,
490 \ 'typeDefinition': {
491 \ 'dynamicRegistration': v:false,
493 \ 'implementation': {
494 \ 'dynamicRegistration': v:false,
495 \ 'linkSupport': v:false,
498 \ 'dynamicRegistration': v:true,
499 \ 'relatedDocumentSupport': v:true,
501 \ 'publishDiagnostics': {
502 \ 'relatedInformation': v:true,
505 \ 'dynamicRegistration': v:false,
506 \ 'codeActionLiteralSupport': {
507 \ 'codeActionKind': {
513 \ 'dynamicRegistration': v:false,
519 let a:conn.init_request_id = l:init_id
520 call s:SendMessageData(a:conn, l:init_data)
523 " Start a program for LSP servers.
525 " 1 will be returned if the program is running, or 0 if the program could
527 function! ale#lsp#StartProgram(conn_id, executable, command) abort
528 let l:conn = s:connections[a:conn_id]
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]]
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)', {
543 \ 'root_dir': l:conn.root,
544 \ 'init_options': l:conn.init_options,
548 let l:conn.client_id = l:client_id
551 return l:client_id > 0
554 if !has_key(l:conn, 'job_id') || !ale#job#HasOpenChannel(l:conn.job_id)
557 \ 'out_cb': {_, message -> ale#lsp#HandleMessage(a:conn_id, message)},
558 \ 'exit_cb': { -> ale#lsp#Stop(a:conn_id) },
562 let l:job_id = ale#job#StartWithCmd(a:command, l:options)
564 let l:job_id = ale#job#Start(a:command, l:options)
569 let l:job_id = l:conn.job_id
573 let l:conn.job_id = l:job_id
576 if l:started && !l:conn.is_tsserver
577 let l:conn.initialized = 0
578 call s:SendInitMessage(l:conn)
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+)$')
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
593 return [l:host, l:port ? l:port : v:null]
596 return [a:address, v:null]
599 " Connect to an LSP server via TCP.
601 " 1 will be returned if the connection is running, or 0 if the connection could
603 function! ale#lsp#ConnectToAddress(conn_id, address) abort
604 let l:conn = s:connections[a:conn_id]
607 if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver
608 let [l:host, l:port] = ale#lsp#SplitAddress(a:address)
610 let l:client_id = luaeval('require("ale.lsp").start(_A)', {
614 \ 'root_dir': l:conn.root,
615 \ 'init_options': l:conn.init_options,
619 let l:conn.client_id = l:client_id
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)},
630 let l:channel_id = l:conn.channel_id
634 let l:conn.channel_id = l:channel_id
638 call s:SendInitMessage(l:conn)
641 return l:channel_id >= 0
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, {})
650 " Add the callback to the List if it's not there already.
651 call uniq(sort(add(l:conn.callback_list, a:callback)))
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)
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)
668 function! ale#lsp#CloseDocument(conn_id) abort
671 " Stop all LSP connections, closing all jobs and channels, and removing any
673 function! ale#lsp#StopAll() abort
674 for l:conn_id in keys(s:connections)
675 call ale#lsp#Stop(l:conn_id)
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)
692 " Send a message to an LSP server.
693 " Notifications do not need to be handled.
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, {})
705 if !l:conn.initialized
706 throw 'LSP server not initialized yet!'
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)
718 let [l:id, l:data] = ale#lsp#CreateMessageData(a:message)
719 call s:SendMessageData(l:conn, l:data)
721 return l:id == 0 ? -1 : l:id
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')
729 return getbufvar(a:buffer, '&filetype')
732 return type(l:Language) is v:t_func ? l:Language(a:buffer) : l:Language
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, {})
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)', {
748 \ 'client_id': l:conn.client_id,
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)
756 let l:conn.open_documents[a:buffer] = getbufvar(a:buffer, 'changedtick')
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.
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
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]
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)', {
783 \ 'client_id': l:conn.client_id,
786 let l:message = ale#lsp#message#DidClose(a:buffer)
787 call ale#lsp#Send(l:conn_id, l:message)
790 call remove(l:conn.open_documents, a:buffer)
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, {})
804 if !empty(l:conn) && has_key(l:conn.open_documents, a:buffer)
805 let l:new_tick = getbufvar(a:buffer, 'changedtick')
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)
811 let l:message = ale#lsp#message#DidChange(a:buffer)
814 call ale#lsp#Send(a:conn_id, l:message)
815 let l:conn.open_documents[a:buffer] = l:new_tick
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, {})
831 if l:conn.initialized
834 call add(l:conn.init_queue, a:Callback)
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, {})
846 if type(get(l:conn.capabilities, a:capability, v:null)) isnot v:t_number
847 throw 'Invalid capability ' . a:capability
850 return l:conn.capabilities[a:capability]