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: Integration between linters and LSP/tsserver.
4 " This code isn't loaded if a user never users LSP features or linters.
6 " Associates LSP connection IDs with linter names.
7 if !has_key(s:, 'lsp_linter_map')
8 let s:lsp_linter_map = {}
11 " Clear LSP linter data for the linting engine.
12 function! ale#lsp_linter#ClearLSPData() abort
13 let s:lsp_linter_map = {}
16 " Only for internal use.
17 function! ale#lsp_linter#GetLSPLinterMap() abort
18 return s:lsp_linter_map
22 function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort
23 let s:lsp_linter_map = a:replacement_map
26 " A map for tracking URIs for diagnostic request IDs
27 if !has_key(s:, 'diagnostic_uri_map')
28 let s:diagnostic_uri_map = {}
31 " For internal use only.
32 function! ale#lsp_linter#ClearDiagnosticURIMap() abort
33 let s:diagnostic_uri_map = {}
36 " For internal use only.
37 function! ale#lsp_linter#GetDiagnosticURIMap() abort
38 return s:diagnostic_uri_map
42 function! ale#lsp_linter#SetDiagnosticURIMap(replacement_map) abort
43 let s:diagnostic_uri_map = a:replacement_map
46 " Get all enabled LSP linters.
47 " This list still includes linters ignored with `ale_linters_ignore`.
49 " `ale_linters_ignore` is designed to allow language servers to be used for
50 " their functionality while ignoring the diagnostics they return.
51 function! ale#lsp_linter#GetEnabled(buffer) abort
52 let l:filetype = getbufvar(a:buffer, '&filetype')
53 " Only LSP linters are included here.
54 let l:linters = filter(ale#linter#Get(l:filetype), '!empty(v:val.lsp)')
55 let l:disable_lsp = ale#Var(a:buffer, 'disable_lsp')
57 " Only load code for ignoring linters if we need it.
60 \ || l:disable_lsp is v:true
61 \ || (l:disable_lsp is# 'auto' && get(g:, 'lspconfig', 0))
63 let l:linters = ale#engine#ignore#Exclude(
74 " Check if diagnostics for a particular linter should be ignored.
75 function! s:ShouldIgnoreDiagnostics(buffer, linter) abort
76 let l:config = ale#Var(a:buffer, 'linters_ignore')
77 let l:disable_lsp = ale#Var(a:buffer, 'disable_lsp')
79 " Only load code for ignoring linters if we need it.
82 \ || l:disable_lsp is 1
83 \ || l:disable_lsp is v:true
84 \ || (l:disable_lsp is# 'auto' && get(g:, 'lspconfig', 0))
86 " Re-use the ignore implementation just for this linter.
88 \ ale#engine#ignore#Exclude(
89 \ getbufvar(a:buffer, '&filetype'),
100 " Handle LSP diagnostics for a given URI.
101 " The special value 'unchanged' can be used for diagnostics to indicate
102 " that diagnostics haven't changed since we last checked.
103 function! ale#lsp_linter#HandleLSPDiagnostics(conn_id, uri, diagnostics) abort
104 let l:linter = get(s:lsp_linter_map, a:conn_id)
110 let l:filename = ale#util#ToResource(a:uri)
111 let l:escaped_name = escape(
112 \ fnameescape(l:filename),
113 \ has('win32') ? '^' : '^,}]'
115 let l:buffer = bufnr('^' . l:escaped_name . '$')
116 let l:info = get(g:ale_buffer_info, l:buffer, {})
122 if s:ShouldIgnoreDiagnostics(l:buffer, l:linter)
126 if a:diagnostics is# 'unchanged'
127 call ale#engine#MarkLinterInactive(l:info, l:linter)
129 let l:loclist = ale#lsp#response#ReadDiagnostics(a:diagnostics)
130 call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist, 0)
134 function! s:HandleTSServerDiagnostics(response, error_type) abort
135 " Re-create a fake linter object for tsserver.
137 \ 'name': 'tsserver',
141 let l:escaped_name = escape(
142 \ fnameescape(a:response.body.file),
143 \ has('win32') ? '^' : '^,}]'
145 let l:buffer = bufnr('^' . l:escaped_name . '$')
146 let l:info = get(g:ale_buffer_info, l:buffer, {})
152 call ale#engine#MarkLinterInactive(l:info, l:linter.name)
154 if s:ShouldIgnoreDiagnostics(l:buffer, l:linter)
158 let l:thislist = ale#lsp#response#ReadTSServerDiagnostics(a:response)
161 " tsserver sends syntax and semantic errors in separate messages, so we
162 " have to collect the messages separately for each buffer and join them
163 " back together again.
164 if a:error_type is# 'syntax'
165 if len(l:thislist) is 0 && len(get(l:info, 'syntax_loclist', [])) is 0
169 let l:info.syntax_loclist = l:thislist
170 elseif a:error_type is# 'semantic'
171 if len(l:thislist) is 0 && len(get(l:info, 'semantic_loclist', [])) is 0
175 let l:info.semantic_loclist = l:thislist
177 if len(l:thislist) is 0 && len(get(l:info, 'suggestion_loclist', [])) is 0
181 let l:info.suggestion_loclist = l:thislist
188 let l:loclist = get(l:info, 'semantic_loclist', [])
189 \ + get(l:info, 'suggestion_loclist', [])
190 \ + get(l:info, 'syntax_loclist', [])
192 call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist, 0)
195 function! s:HandleLSPErrorMessage(linter, response) abort
196 if !g:ale_history_enabled || !g:ale_history_log_output
204 let l:message = ale#lsp#response#GetErrorMessage(a:response)
210 call ale#lsp_linter#AddErrorMessage(a:linter.name, l:message)
213 function! ale#lsp_linter#AddErrorMessage(linter_name, message) abort
214 " This global variable is set here so we don't load the debugging.vim file
215 " until someone uses :ALEInfo.
216 let g:ale_lsp_error_messages = get(g:, 'ale_lsp_error_messages', {})
218 if !has_key(g:ale_lsp_error_messages, a:linter_name)
219 let g:ale_lsp_error_messages[a:linter_name] = []
222 call add(g:ale_lsp_error_messages[a:linter_name], a:message)
225 function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort
226 let l:method = get(a:response, 'method', '')
228 if get(a:response, 'jsonrpc', '') is# '2.0' && has_key(a:response, 'error')
229 let l:linter = get(s:lsp_linter_map, a:conn_id, {})
231 call s:HandleLSPErrorMessage(l:linter, a:response)
232 elseif l:method is# 'textDocument/publishDiagnostics'
233 let l:uri = a:response.params.uri
234 let l:diagnostics = a:response.params.diagnostics
236 call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
237 elseif has_key(s:diagnostic_uri_map, get(a:response, 'id'))
238 let l:uri = remove(s:diagnostic_uri_map, a:response.id)
239 let l:diagnostics = a:response.result.kind is# 'unchanged'
241 \ : a:response.result.items
243 call ale#lsp_linter#HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
244 elseif l:method is# 'window/showMessage'
245 call ale#lsp_window#HandleShowMessage(
246 \ s:lsp_linter_map[a:conn_id].name,
247 \ g:ale_lsp_show_message_format,
250 elseif get(a:response, 'type', '') is# 'event'
251 \&& get(a:response, 'event', '') is# 'semanticDiag'
252 call s:HandleTSServerDiagnostics(a:response, 'semantic')
253 elseif get(a:response, 'type', '') is# 'event'
254 \&& get(a:response, 'event', '') is# 'syntaxDiag'
255 call s:HandleTSServerDiagnostics(a:response, 'syntax')
256 elseif get(a:response, 'type', '') is# 'event'
257 \&& get(a:response, 'event', '') is# 'suggestionDiag'
258 \&& get(g:, 'ale_lsp_suggestions')
259 call s:HandleTSServerDiagnostics(a:response, 'suggestion')
263 function! ale#lsp_linter#GetOptions(buffer, linter) abort
264 if has_key(a:linter, 'initialization_options_callback')
265 return ale#util#GetFunction(a:linter.initialization_options_callback)(a:buffer)
268 if has_key(a:linter, 'initialization_options')
269 let l:Options = a:linter.initialization_options
271 if type(l:Options) is v:t_func
272 let l:Options = l:Options(a:buffer)
281 function! ale#lsp_linter#GetConfig(buffer, linter) abort
282 if has_key(a:linter, 'lsp_config_callback')
283 return ale#util#GetFunction(a:linter.lsp_config_callback)(a:buffer)
286 if has_key(a:linter, 'lsp_config')
287 let l:Config = a:linter.lsp_config
289 if type(l:Config) is v:t_func
290 let l:Config = l:Config(a:buffer)
299 function! ale#lsp_linter#FindProjectRoot(buffer, linter) abort
300 let l:buffer_ale_root = getbufvar(a:buffer, 'ale_root', {})
302 if type(l:buffer_ale_root) is v:t_string
303 return l:buffer_ale_root
306 " Try to get a buffer-local setting for the root
307 if has_key(l:buffer_ale_root, a:linter.name)
308 let l:Root = l:buffer_ale_root[a:linter.name]
310 if type(l:Root) is v:t_func
311 return l:Root(a:buffer)
317 " Try to get a global setting for the root
318 if has_key(g:ale_root, a:linter.name)
319 let l:Root = g:ale_root[a:linter.name]
321 if type(l:Root) is v:t_func
322 return l:Root(a:buffer)
328 " Fall back to the linter-specific configuration
329 if has_key(a:linter, 'project_root')
330 let l:Root = a:linter.project_root
332 return type(l:Root) is v:t_func ? l:Root(a:buffer) : l:Root
335 return ale#util#GetFunction(a:linter.project_root_callback)(a:buffer)
338 " This function is accessible so tests can call it.
339 function! ale#lsp_linter#OnInit(linter, details, Callback) abort
340 let l:buffer = a:details.buffer
341 let l:conn_id = a:details.connection_id
342 let l:command = a:details.command
344 let l:config = ale#lsp_linter#GetConfig(l:buffer, a:linter)
346 call ale#lsp#UpdateConfig(l:conn_id, l:buffer, l:config)
348 if ale#lsp#OpenDocument(l:conn_id, l:buffer)
349 if g:ale_history_enabled && !empty(l:command)
350 call ale#history#Add(l:buffer, 'started', l:conn_id, l:command)
354 " The change message needs to be sent for tsserver before doing anything.
355 if a:linter.lsp is# 'tsserver'
356 call ale#lsp#NotifyForChanges(l:conn_id, l:buffer)
359 " Tell the relevant buffer that the LSP has started via an autocmd.
361 if l:buffer == bufnr('')
362 silent doautocmd <nomodeline> User ALELSPStarted
364 execute 'augroup ALELSPStartedGroup' . l:buffer
368 \ 'autocmd BufEnter <buffer=%d>'
369 \ . ' doautocmd <nomodeline> User ALELSPStarted',
373 " Replicate ++once behavior for backwards compatibility.
375 \ 'autocmd BufEnter <buffer=%d>'
376 \ . ' autocmd! ALELSPStartedGroup%d',
383 call a:Callback(a:linter, a:details)
386 function! s:StartLSP(options, address, executable, command) abort
387 let l:buffer = a:options.buffer
388 let l:linter = a:options.linter
389 let l:root = a:options.root
390 let l:Callback = a:options.callback
392 let l:init_options = ale#lsp_linter#GetOptions(l:buffer, l:linter)
394 if l:linter.lsp is# 'socket'
395 let l:conn_id = ale#lsp#Register(
401 let l:ready = ale#lsp#ConnectToAddress(l:conn_id, a:address)
404 let l:conn_id = ale#lsp#Register(
411 " tsserver behaves differently, so tell the LSP API that it is tsserver.
412 if l:linter.lsp is# 'tsserver'
413 call ale#lsp#MarkConnectionAsTsserver(l:conn_id)
416 let l:cwd = ale#linter#GetCwd(l:buffer, l:linter)
417 let l:command = ale#command#FormatCommand(
424 \ ale#GetFilenameMappings(l:buffer, l:linter.name),
426 let l:command = ale#job#PrepareCommand(l:buffer, l:command)
427 let l:ready = ale#lsp#StartProgram(l:conn_id, a:executable, l:command)
431 if g:ale_history_enabled && !empty(a:command)
432 call ale#history#Add(l:buffer, 'failed', l:conn_id, a:command)
439 \ 'buffer': l:buffer,
440 \ 'connection_id': l:conn_id,
441 \ 'command': l:command,
442 \ 'project_root': l:root,
445 call ale#lsp#OnInit(l:conn_id, {->
446 \ ale#lsp_linter#OnInit(l:linter, l:details, l:Callback)
452 function! s:StartWithAddress(options, address) abort
453 if ale#command#IsDeferred(a:address)
454 let a:address.result_callback = {
455 \ address -> s:StartWithAddress(a:options, address)
465 return s:StartLSP(a:options, a:address, '', '')
468 function! s:StartWithCommand(options, executable, command) abort
469 if ale#command#IsDeferred(a:command)
470 let a:command.result_callback = {
471 \ command -> s:StartWithCommand(a:options, a:executable, command)
481 return s:StartLSP(a:options, '', a:executable, a:command)
484 function! s:StartIfExecutable(options, executable) abort
485 if ale#command#IsDeferred(a:executable)
486 let a:executable.result_callback = {
487 \ executable -> s:StartIfExecutable(a:options, executable)
493 if !ale#engine#IsExecutable(a:options.buffer, a:executable)
497 let l:command = ale#linter#GetCommand(a:options.buffer, a:options.linter)
499 return s:StartWithCommand(a:options, a:executable, l:command)
502 " Given a buffer, an LSP linter, start up an LSP linter and get ready to
503 " receive messages for the document.
504 function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort
507 let l:root = ale#lsp_linter#FindProjectRoot(a:buffer, a:linter)
509 if empty(l:root) && a:linter.lsp isnot# 'tsserver'
510 " If there's no project root, then we can't check files with LSP,
511 " unless we are using tsserver, which doesn't use project roots.
512 call ale#lsp_linter#AddErrorMessage(a:linter.name, "Failed to find project root, language server won't start.")
518 \ 'buffer': a:buffer,
519 \ 'linter': a:linter,
520 \ 'callback': a:Callback,
524 if a:linter.lsp is# 'socket'
525 let l:address = ale#linter#GetAddress(a:buffer, a:linter)
527 return s:StartWithAddress(l:options, l:address)
530 let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
532 return s:StartIfExecutable(l:options, l:executable)
535 function! s:CheckWithLSP(linter, details) abort
536 let l:buffer = a:details.buffer
537 let l:info = get(g:ale_buffer_info, l:buffer)
543 let l:id = a:details.connection_id
545 " Register a callback now for handling errors now.
546 let l:Callback = function('ale#lsp_linter#HandleLSPResponse')
547 call ale#lsp#RegisterCallback(l:id, l:Callback)
549 " Remember the linter this connection is for.
550 let s:lsp_linter_map[l:id] = a:linter
552 if a:linter.lsp is# 'tsserver'
553 let l:message = ale#lsp#tsserver_message#Geterr(l:buffer)
554 let l:notified = ale#lsp#Send(l:id, l:message) != 0
557 call ale#engine#MarkLinterActive(l:info, a:linter)
559 elseif !g:ale_use_neovim_lsp_api
560 let l:notified = ale#lsp#NotifyForChanges(l:id, l:buffer)
562 " If this was a file save event, also notify the server of that.
563 if getbufvar(l:buffer, 'ale_save_event_fired', 0)
564 \&& ale#lsp#HasCapability(l:id, 'did_save')
565 let l:include_text = ale#lsp#HasCapability(l:id, 'includeText')
566 let l:save_message = ale#lsp#message#DidSave(l:buffer, l:include_text)
567 let l:notified = ale#lsp#Send(l:id, l:save_message) != 0
570 let l:diagnostic_request_id = 0
572 " If the document is updated and we can pull diagnostics, try to.
573 if ale#lsp#HasCapability(l:id, 'pull_model')
574 let l:diagnostic_message = ale#lsp#message#Diagnostic(l:buffer)
576 let l:diagnostic_request_id = ale#lsp#Send(l:id, l:diagnostic_message)
579 " If we are going to pull diagnostics, then mark the linter as active,
580 " and remember the URI we sent the request for.
581 if l:diagnostic_request_id
582 call ale#engine#MarkLinterActive(l:info, a:linter)
583 let s:diagnostic_uri_map[l:diagnostic_request_id] =
584 \ l:diagnostic_message[2].textDocument.uri
589 function! ale#lsp_linter#CheckWithLSP(buffer, linter) abort
590 return ale#lsp_linter#StartLSP(a:buffer, a:linter, function('s:CheckWithLSP'))
593 function! s:HandleLSPResponseToCustomRequests(conn_id, response) abort
594 if has_key(a:response, 'id')
595 " Get the custom handlers Dictionary from the linter map.
596 let l:linter = get(s:lsp_linter_map, a:conn_id, {})
597 let l:custom_handlers = get(l:linter, 'custom_handlers', {})
599 if has_key(l:custom_handlers, a:response.id)
600 let l:Handler = remove(l:custom_handlers, a:response.id)
601 call l:Handler(a:response)
606 function! s:OnReadyForCustomRequests(args, linter, lsp_details) abort
607 let l:id = a:lsp_details.connection_id
608 let l:request_id = ale#lsp#Send(l:id, a:args.message)
610 if l:request_id > 0 && has_key(a:args, 'handler')
611 let l:Callback = function('s:HandleLSPResponseToCustomRequests')
612 call ale#lsp#RegisterCallback(l:id, l:Callback)
614 " Remember the linter this connection is for.
615 let s:lsp_linter_map[l:id] = a:linter
617 " Add custom_handlers to the linter Dictionary.
618 if !has_key(a:linter, 'custom_handlers')
619 let a:linter.custom_handlers = {}
622 " Put the handler function in the map to call later.
623 let a:linter.custom_handlers[l:request_id] = a:args.handler
627 " Send a custom request to an LSP linter.
628 function! ale#lsp_linter#SendRequest(buffer, linter_name, message, ...) abort
629 let l:filetype = ale#linter#ResolveFiletype(getbufvar(a:buffer, '&filetype'))
630 let l:linter_list = ale#linter#GetAll(l:filetype)
631 let l:linter_list = filter(l:linter_list, {_, v -> v.name is# a:linter_name})
633 if len(l:linter_list) < 1
634 throw 'Linter "' . a:linter_name . '" not found!'
637 let l:linter = l:linter_list[0]
639 if empty(l:linter.lsp)
640 throw 'Linter "' . a:linter_name . '" does not support LSP!'
643 let l:is_notification = a:message[0]
644 let l:callback_args = {'message': a:message}
646 if !l:is_notification && a:0
647 let l:callback_args.handler = a:1
650 let l:Callback = function('s:OnReadyForCustomRequests', [l:callback_args])
652 return ale#lsp_linter#StartLSP(a:buffer, l:linter, l:Callback)