]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/lsp_linter.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_linter.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Integration between linters and LSP/tsserver.
3
4 " This code isn't loaded if a user never users LSP features or linters.
5
6 " Associates LSP connection IDs with linter names.
7 if !has_key(s:, 'lsp_linter_map')
8     let s:lsp_linter_map = {}
9 endif
10
11 " Clear LSP linter data for the linting engine.
12 function! ale#lsp_linter#ClearLSPData() abort
13     let s:lsp_linter_map = {}
14 endfunction
15
16 " Only for internal use.
17 function! ale#lsp_linter#GetLSPLinterMap() abort
18     return s:lsp_linter_map
19 endfunction
20
21 " Just for tests.
22 function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort
23     let s:lsp_linter_map = a:replacement_map
24 endfunction
25
26 " A map for tracking URIs for diagnostic request IDs
27 if !has_key(s:, 'diagnostic_uri_map')
28     let s:diagnostic_uri_map = {}
29 endif
30
31 " For internal use only.
32 function! ale#lsp_linter#ClearDiagnosticURIMap() abort
33     let s:diagnostic_uri_map = {}
34 endfunction
35
36 " For internal use only.
37 function! ale#lsp_linter#GetDiagnosticURIMap() abort
38     return s:diagnostic_uri_map
39 endfunction
40
41 " Just for tests.
42 function! ale#lsp_linter#SetDiagnosticURIMap(replacement_map) abort
43     let s:diagnostic_uri_map = a:replacement_map
44 endfunction
45
46 " Get all enabled LSP linters.
47 " This list still includes linters ignored with `ale_linters_ignore`.
48 "
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')
56
57     " Only load code for ignoring linters if we need it.
58     if (
59     \   l:disable_lsp is 1
60     \   || l:disable_lsp is v:true
61     \   || (l:disable_lsp is# 'auto' && get(g:, 'lspconfig', 0))
62     \)
63         let l:linters = ale#engine#ignore#Exclude(
64         \   l:filetype,
65         \   l:linters,
66         \   [],
67         \   l:disable_lsp,
68         \)
69     endif
70
71     return l:linters
72 endfunction
73
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')
78
79     " Only load code for ignoring linters if we need it.
80     if (
81     \   !empty(l:config)
82     \   || l:disable_lsp is 1
83     \   || l:disable_lsp is v:true
84     \   || (l:disable_lsp is# 'auto' && get(g:, 'lspconfig', 0))
85     \)
86         " Re-use the ignore implementation just for this linter.
87         return empty(
88         \   ale#engine#ignore#Exclude(
89         \       getbufvar(a:buffer, '&filetype'),
90         \       [a:linter],
91         \       l:config,
92         \       l:disable_lsp,
93         \   )
94         \)
95     endif
96
97     return 0
98 endfunction
99
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)
105
106     if empty(l:linter)
107         return
108     endif
109
110     let l:filename = ale#util#ToResource(a:uri)
111     let l:escaped_name = escape(
112     \   fnameescape(l:filename),
113     \   has('win32') ? '^' : '^,}]'
114     \)
115     let l:buffer = bufnr('^' . l:escaped_name . '$')
116     let l:info = get(g:ale_buffer_info, l:buffer, {})
117
118     if empty(l:info)
119         return
120     endif
121
122     if s:ShouldIgnoreDiagnostics(l:buffer, l:linter)
123         return
124     endif
125
126     if a:diagnostics is# 'unchanged'
127         call ale#engine#MarkLinterInactive(l:info, l:linter)
128     else
129         let l:loclist = ale#lsp#response#ReadDiagnostics(a:diagnostics)
130         call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist, 0)
131     endif
132 endfunction
133
134 function! s:HandleTSServerDiagnostics(response, error_type) abort
135     " Re-create a fake linter object for tsserver.
136     let l:linter = {
137     \   'name': 'tsserver',
138     \   'aliases': [],
139     \   'lsp': 'tsserver',
140     \}
141     let l:escaped_name = escape(
142     \   fnameescape(a:response.body.file),
143     \   has('win32') ? '^' : '^,}]'
144     \)
145     let l:buffer = bufnr('^' . l:escaped_name . '$')
146     let l:info = get(g:ale_buffer_info, l:buffer, {})
147
148     if empty(l:info)
149         return
150     endif
151
152     call ale#engine#MarkLinterInactive(l:info, l:linter.name)
153
154     if s:ShouldIgnoreDiagnostics(l:buffer, l:linter)
155         return
156     endif
157
158     let l:thislist = ale#lsp#response#ReadTSServerDiagnostics(a:response)
159     let l:no_changes = 0
160
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
166             let l:no_changes = 1
167         endif
168
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
172             let l:no_changes = 1
173         endif
174
175         let l:info.semantic_loclist = l:thislist
176     else
177         if len(l:thislist) is 0 && len(get(l:info, 'suggestion_loclist', [])) is 0
178             let l:no_changes = 1
179         endif
180
181         let l:info.suggestion_loclist = l:thislist
182     endif
183
184     if l:no_changes
185         return
186     endif
187
188     let l:loclist = get(l:info, 'semantic_loclist', [])
189     \   + get(l:info, 'suggestion_loclist', [])
190     \   + get(l:info, 'syntax_loclist', [])
191
192     call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist, 0)
193 endfunction
194
195 function! s:HandleLSPErrorMessage(linter, response) abort
196     if !g:ale_history_enabled || !g:ale_history_log_output
197         return
198     endif
199
200     if empty(a:linter)
201         return
202     endif
203
204     let l:message = ale#lsp#response#GetErrorMessage(a:response)
205
206     if empty(l:message)
207         return
208     endif
209
210     call ale#lsp_linter#AddErrorMessage(a:linter.name, l:message)
211 endfunction
212
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', {})
217
218     if !has_key(g:ale_lsp_error_messages, a:linter_name)
219         let g:ale_lsp_error_messages[a:linter_name] = []
220     endif
221
222     call add(g:ale_lsp_error_messages[a:linter_name], a:message)
223 endfunction
224
225 function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort
226     let l:method = get(a:response, 'method', '')
227
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, {})
230
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
235
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'
240         \   ? 'unchanged'
241         \   : a:response.result.items
242
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,
248         \   a:response.params
249         \)
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')
260     endif
261 endfunction
262
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)
266     endif
267
268     if has_key(a:linter, 'initialization_options')
269         let l:Options = a:linter.initialization_options
270
271         if type(l:Options) is v:t_func
272             let l:Options = l:Options(a:buffer)
273         endif
274
275         return l:Options
276     endif
277
278     return {}
279 endfunction
280
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)
284     endif
285
286     if has_key(a:linter, 'lsp_config')
287         let l:Config = a:linter.lsp_config
288
289         if type(l:Config) is v:t_func
290             let l:Config = l:Config(a:buffer)
291         endif
292
293         return l:Config
294     endif
295
296     return {}
297 endfunction
298
299 function! ale#lsp_linter#FindProjectRoot(buffer, linter) abort
300     let l:buffer_ale_root = getbufvar(a:buffer, 'ale_root', {})
301
302     if type(l:buffer_ale_root) is v:t_string
303         return l:buffer_ale_root
304     endif
305
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]
309
310         if type(l:Root) is v:t_func
311             return l:Root(a:buffer)
312         else
313             return l:Root
314         endif
315     endif
316
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]
320
321         if type(l:Root) is v:t_func
322             return l:Root(a:buffer)
323         else
324             return l:Root
325         endif
326     endif
327
328     " Fall back to the linter-specific configuration
329     if has_key(a:linter, 'project_root')
330         let l:Root = a:linter.project_root
331
332         return type(l:Root) is v:t_func ? l:Root(a:buffer) : l:Root
333     endif
334
335     return ale#util#GetFunction(a:linter.project_root_callback)(a:buffer)
336 endfunction
337
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
343
344     let l:config = ale#lsp_linter#GetConfig(l:buffer, a:linter)
345
346     call ale#lsp#UpdateConfig(l:conn_id, l:buffer, l:config)
347
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)
351         endif
352     endif
353
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)
357     endif
358
359     " Tell the relevant buffer that the LSP has started via an autocmd.
360     if l:buffer > 0
361         if l:buffer == bufnr('')
362             silent doautocmd <nomodeline> User ALELSPStarted
363         else
364             execute 'augroup ALELSPStartedGroup' . l:buffer
365                 autocmd!
366
367                 execute printf(
368                 \   'autocmd BufEnter <buffer=%d>'
369                 \       . ' doautocmd <nomodeline> User ALELSPStarted',
370                 \   l:buffer
371                 \)
372
373                 " Replicate ++once behavior for backwards compatibility.
374                 execute printf(
375                 \   'autocmd BufEnter <buffer=%d>'
376                 \       . ' autocmd! ALELSPStartedGroup%d',
377                 \   l:buffer, l:buffer
378                 \)
379             augroup END
380         endif
381     endif
382
383     call a:Callback(a:linter, a:details)
384 endfunction
385
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
391
392     let l:init_options = ale#lsp_linter#GetOptions(l:buffer, l:linter)
393
394     if l:linter.lsp is# 'socket'
395         let l:conn_id = ale#lsp#Register(
396         \   a:address,
397         \   l:root,
398         \   l:linter.language,
399         \   l:init_options
400         \)
401         let l:ready = ale#lsp#ConnectToAddress(l:conn_id, a:address)
402         let l:command = ''
403     else
404         let l:conn_id = ale#lsp#Register(
405         \   a:executable,
406         \   l:root,
407         \   l:linter.language,
408         \   l:init_options
409         \)
410
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)
414         endif
415
416         let l:cwd = ale#linter#GetCwd(l:buffer, l:linter)
417         let l:command = ale#command#FormatCommand(
418         \   l:buffer,
419         \   a:executable,
420         \   a:command,
421         \   0,
422         \   v:false,
423         \   l:cwd,
424         \   ale#GetFilenameMappings(l:buffer, l:linter.name),
425         \)[1]
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)
428     endif
429
430     if !l:ready
431         if g:ale_history_enabled && !empty(a:command)
432             call ale#history#Add(l:buffer, 'failed', l:conn_id, a:command)
433         endif
434
435         return 0
436     endif
437
438     let l:details = {
439     \   'buffer': l:buffer,
440     \   'connection_id': l:conn_id,
441     \   'command': l:command,
442     \   'project_root': l:root,
443     \}
444
445     call ale#lsp#OnInit(l:conn_id, {->
446     \   ale#lsp_linter#OnInit(l:linter, l:details, l:Callback)
447     \})
448
449     return 1
450 endfunction
451
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)
456         \}
457
458         return 1
459     endif
460
461     if empty(a:address)
462         return 0
463     endif
464
465     return s:StartLSP(a:options, a:address, '', '')
466 endfunction
467
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)
472         \}
473
474         return 1
475     endif
476
477     if empty(a:command)
478         return 0
479     endif
480
481     return s:StartLSP(a:options, '', a:executable, a:command)
482 endfunction
483
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)
488         \}
489
490         return 1
491     endif
492
493     if !ale#engine#IsExecutable(a:options.buffer, a:executable)
494         return 0
495     endif
496
497     let l:command = ale#linter#GetCommand(a:options.buffer, a:options.linter)
498
499     return s:StartWithCommand(a:options, a:executable, l:command)
500 endfunction
501
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
505     let l:command = ''
506     let l:address = ''
507     let l:root = ale#lsp_linter#FindProjectRoot(a:buffer, a:linter)
508
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.")
513
514         return 0
515     endif
516
517     let l:options = {
518     \   'buffer': a:buffer,
519     \   'linter': a:linter,
520     \   'callback': a:Callback,
521     \   'root': l:root,
522     \}
523
524     if a:linter.lsp is# 'socket'
525         let l:address = ale#linter#GetAddress(a:buffer, a:linter)
526
527         return s:StartWithAddress(l:options, l:address)
528     endif
529
530     let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
531
532     return s:StartIfExecutable(l:options, l:executable)
533 endfunction
534
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)
538
539     if empty(l:info)
540         return
541     endif
542
543     let l:id = a:details.connection_id
544
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)
548
549     " Remember the linter this connection is for.
550     let s:lsp_linter_map[l:id] = a:linter
551
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
555
556         if l:notified
557             call ale#engine#MarkLinterActive(l:info, a:linter)
558         endif
559     elseif !g:ale_use_neovim_lsp_api
560         let l:notified = ale#lsp#NotifyForChanges(l:id, l:buffer)
561
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
568         endif
569
570         let l:diagnostic_request_id = 0
571
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)
575
576             let l:diagnostic_request_id = ale#lsp#Send(l:id, l:diagnostic_message)
577         endif
578
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
585         endif
586     endif
587 endfunction
588
589 function! ale#lsp_linter#CheckWithLSP(buffer, linter) abort
590     return ale#lsp_linter#StartLSP(a:buffer, a:linter, function('s:CheckWithLSP'))
591 endfunction
592
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', {})
598
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)
602         endif
603     endif
604 endfunction
605
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)
609
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)
613
614         " Remember the linter this connection is for.
615         let s:lsp_linter_map[l:id] = a:linter
616
617         " Add custom_handlers to the linter Dictionary.
618         if !has_key(a:linter, 'custom_handlers')
619             let a:linter.custom_handlers = {}
620         endif
621
622         " Put the handler function in the map to call later.
623         let a:linter.custom_handlers[l:request_id] = a:args.handler
624     endif
625 endfunction
626
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})
632
633     if len(l:linter_list) < 1
634         throw 'Linter "' . a:linter_name . '" not found!'
635     endif
636
637     let l:linter = l:linter_list[0]
638
639     if empty(l:linter.lsp)
640         throw 'Linter "' . a:linter_name . '" does not support LSP!'
641     endif
642
643     let l:is_notification = a:message[0]
644     let l:callback_args = {'message': a:message}
645
646     if !l:is_notification && a:0
647         let l:callback_args.handler = a:1
648     endif
649
650     let l:Callback = function('s:OnReadyForCustomRequests', [l:callback_args])
651
652     return ale#lsp_linter#StartLSP(a:buffer, l:linter, l:Callback)
653 endfunction