Skip to content

Commit f90e72a

Browse files
committed
Close dense-analysis#3600 - Implement pull diagnostics in VimL
Implement pull diagnostics in the VimL implementation so ALE is able to track when servers are busy checking files. Only servers that support this feature will return diagnostics these ways.
1 parent 26ffb9d commit f90e72a

9 files changed

+248
-27
lines changed

autoload/ale/lsp.vim

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function! ale#lsp#Register(executable_or_address, project, language, init_option
4747
\ 'definition': 0,
4848
\ 'typeDefinition': 0,
4949
\ 'implementation': 0,
50+
\ 'pull_model': 0,
5051
\ 'symbol_search': 0,
5152
\ 'code_actions': 0,
5253
\ 'did_save': 0,
@@ -276,6 +277,13 @@ function! ale#lsp#UpdateCapabilities(conn_id, capabilities) abort
276277
let l:conn.capabilities.implementation = 1
277278
endif
278279

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+
279287
if get(a:capabilities, 'workspaceSymbolProvider') is v:true
280288
let l:conn.capabilities.symbol_search = 1
281289
endif
@@ -486,6 +494,10 @@ function! s:SendInitMessage(conn) abort
486494
\ 'dynamicRegistration': v:false,
487495
\ 'linkSupport': v:false,
488496
\ },
497+
\ 'diagnostic': {
498+
\ 'dynamicRegistration': v:true,
499+
\ 'relatedDocumentSupport': v:true,
500+
\ },
489501
\ 'publishDiagnostics': {
490502
\ 'relatedInformation': v:true,
491503
\ },

autoload/ale/lsp/message.vim

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column,
200200
\}]
201201
endfunction
202202

203+
function! ale#lsp#message#Diagnostic(buffer) abort
204+
return [0, 'textDocument/diagnostic', {
205+
\ 'textDocument': {
206+
\ 'uri': ale#util#ToURI(expand('#' . a:buffer . ':p')),
207+
\ },
208+
\}]
209+
endfunction
210+
203211
function! ale#lsp#message#ExecuteCommand(command, arguments) abort
204212
return [0, 'workspace/executeCommand', {
205213
\ 'command': a:command,

autoload/ale/lsp/response.vim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ let s:SEVERITY_WARNING = 2
2121
let s:SEVERITY_INFORMATION = 3
2222
let s:SEVERITY_HINT = 4
2323

24-
" Parse the message for textDocument/publishDiagnostics
25-
function! ale#lsp#response#ReadDiagnostics(response) abort
24+
" Convert Diagnostic[] data from a language server to an ALE loclist.
25+
function! ale#lsp#response#ReadDiagnostics(diagnostics) abort
2626
let l:loclist = []
2727

28-
for l:diagnostic in a:response.params.diagnostics
28+
for l:diagnostic in a:diagnostics
2929
let l:severity = get(l:diagnostic, 'severity', 0)
3030
let l:loclist_item = {
3131
\ 'text': substitute(l:diagnostic.message, '\(\r\n\|\n\|\r\)', ' ', 'g'),

autoload/ale/lsp_linter.vim

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort
2323
let s:lsp_linter_map = a:replacement_map
2424
endfunction
2525

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+
2646
" Get all enabled LSP linters.
2747
" This list still includes linters ignored with `ale_linters_ignore`.
2848
"
@@ -77,14 +97,17 @@ function! s:ShouldIgnoreDiagnostics(buffer, linter) abort
7797
return 0
7898
endfunction
7999

80-
function! s:HandleLSPDiagnostics(conn_id, response) abort
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! s:HandleLSPDiagnostics(conn_id, uri, diagnostics) abort
81104
let l:linter = get(s:lsp_linter_map, a:conn_id)
82105

83106
if empty(l:linter)
84107
return
85108
endif
86109

87-
let l:filename = ale#util#ToResource(a:response.params.uri)
110+
let l:filename = ale#util#ToResource(a:uri)
88111
let l:escaped_name = escape(
89112
\ fnameescape(l:filename),
90113
\ has('win32') ? '^' : '^,}]'
@@ -100,9 +123,12 @@ function! s:HandleLSPDiagnostics(conn_id, response) abort
100123
return
101124
endif
102125

103-
let l:loclist = ale#lsp#response#ReadDiagnostics(a:response)
104-
105-
call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist, 0)
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
106132
endfunction
107133

108134
function! s:HandleTSServerDiagnostics(response, error_type) abort
@@ -204,7 +230,17 @@ function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort
204230

205231
call s:HandleLSPErrorMessage(l:linter, a:response)
206232
elseif l:method is# 'textDocument/publishDiagnostics'
207-
call s:HandleLSPDiagnostics(a:conn_id, a:response)
233+
let l:uri = a:response.params.uri
234+
let l:diagnostics = a:response.params.diagnostics
235+
236+
call s: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 s:HandleLSPDiagnostics(a:conn_id, l:uri, l:diagnostics)
208244
elseif l:method is# 'window/showMessage'
209245
call ale#lsp_window#HandleShowMessage(
210246
\ s:lsp_linter_map[a:conn_id].name,
@@ -530,6 +566,23 @@ function! s:CheckWithLSP(linter, details) abort
530566
let l:save_message = ale#lsp#message#DidSave(l:buffer, l:include_text)
531567
let l:notified = ale#lsp#Send(l:id, l:save_message) != 0
532568
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
533586
endif
534587
endfunction
535588

test/lsp/test_engine_lsp_response_handling.vader

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,130 @@ Execute(LSP errors should mark linters no longer active):
410410

411411
AssertEqual [], g:ale_buffer_info[bufnr('')].active_linter_list
412412

413+
Execute(LSP pull model diagnostic responses should be handled):
414+
let b:ale_linters = ['eclipselsp']
415+
runtime ale_linters/java/eclipselsp.vim
416+
417+
if has('win32')
418+
call ale#test#SetFilename('filename,[]^$.ts')
419+
else
420+
call ale#test#SetFilename('filename*?,{}[]^$.java')
421+
endif
422+
423+
call ale#engine#InitBufferInfo(bufnr(''))
424+
let g:ale_buffer_info[bufnr('')].active_linter_list = ale#linter#Get('eclipselsp')
425+
call ale#lsp_linter#SetLSPLinterMap({'1': {'name': 'eclipselsp', 'aliases': [], 'lsp': 'stdio'}})
426+
call ale#lsp_linter#SetDiagnosticURIMap({'347': ale#util#ToURI(expand('%:p'))})
427+
428+
if has('win32')
429+
AssertEqual 'filename,[]^$.ts', expand('%:p:t')
430+
else
431+
AssertEqual 'filename*?,{}[]^$.java', expand('%:p:t')
432+
endif
433+
434+
call ale#lsp_linter#HandleLSPResponse(1, {
435+
\ 'jsonrpc':'2.0',
436+
\ 'id': 347,
437+
\ 'result': {
438+
\ 'kind': 'full',
439+
\ 'items': [
440+
\ {
441+
\ 'range': {
442+
\ 'start': {
443+
\ 'line': 0,
444+
\ 'character':0
445+
\ },
446+
\ 'end': {
447+
\ 'line': 0,
448+
\ 'character':0
449+
\ }
450+
\ },
451+
\ 'severity': 2,
452+
\ 'code': "",
453+
\ 'source': 'Java',
454+
\ 'message': 'Missing JRE 1-8'
455+
\ }
456+
\ ]
457+
\ },
458+
\})
459+
460+
AssertEqual
461+
\ [
462+
\ {
463+
\ 'lnum': 1,
464+
\ 'bufnr': bufnr(''),
465+
\ 'col': 1,
466+
\ 'pattern': '',
467+
\ 'valid': 1,
468+
\ 'vcol': 0,
469+
\ 'nr': -1,
470+
\ 'type': 'W',
471+
\ 'text': 'Missing JRE 1-8'
472+
\ }
473+
\ ],
474+
\ ale#test#GetLoclistWithoutNewerKeys()
475+
AssertEqual [], g:ale_buffer_info[bufnr('')].active_linter_list
476+
477+
Execute(LSP pull model diagnostic responses that are 'unchanged' should be handled):
478+
let b:ale_linters = ['eclipselsp']
479+
runtime ale_linters/java/eclipselsp.vim
480+
481+
if has('win32')
482+
call ale#test#SetFilename('filename,[]^$.ts')
483+
else
484+
call ale#test#SetFilename('filename*?,{}[]^$.java')
485+
endif
486+
487+
call ale#engine#InitBufferInfo(bufnr(''))
488+
let g:ale_buffer_info[bufnr('')].active_linter_list = ale#linter#Get('eclipselsp')
489+
let g:ale_buffer_info[bufnr('')].loclist = [
490+
\ {
491+
\ 'lnum': 1,
492+
\ 'bufnr': bufnr(''),
493+
\ 'col': 1,
494+
\ 'pattern': '',
495+
\ 'valid': 1,
496+
\ 'vcol': 0,
497+
\ 'nr': -1,
498+
\ 'type': 'W',
499+
\ 'text': 'Missing JRE 1-8'
500+
\ },
501+
\]
502+
503+
call ale#lsp_linter#SetLSPLinterMap({'1': {'name': 'eclipselsp', 'aliases': [], 'lsp': 'stdio'}})
504+
call ale#lsp_linter#SetDiagnosticURIMap({'347': ale#util#ToURI(expand('%:p'))})
505+
506+
if has('win32')
507+
AssertEqual 'filename,[]^$.ts', expand('%:p:t')
508+
else
509+
AssertEqual 'filename*?,{}[]^$.java', expand('%:p:t')
510+
endif
511+
512+
call ale#lsp_linter#HandleLSPResponse(1, {
513+
\ 'jsonrpc':'2.0',
514+
\ 'id': 347,
515+
\ 'result': {
516+
\ 'kind': 'unchanged',
517+
\ },
518+
\})
519+
520+
AssertEqual
521+
\ [
522+
\ {
523+
\ 'lnum': 1,
524+
\ 'bufnr': bufnr(''),
525+
\ 'col': 1,
526+
\ 'pattern': '',
527+
\ 'valid': 1,
528+
\ 'vcol': 0,
529+
\ 'nr': -1,
530+
\ 'type': 'W',
531+
\ 'text': 'Missing JRE 1-8'
532+
\ }
533+
\ ],
534+
\ g:ale_buffer_info[bufnr('')].loclist
535+
AssertEqual [], g:ale_buffer_info[bufnr('')].active_linter_list
536+
413537
Execute(LSP errors should be logged in the history):
414538
call ale#lsp_linter#SetLSPLinterMap({'347': {'name': 'foobar', 'aliases': [], 'lsp': 'stdio'}})
415539
call ale#lsp_linter#HandleLSPResponse(347, {

test/lsp/test_lsp_client_messages.vader

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,19 @@ Execute(ale#lsp#message#DidChangeConfiguration() should return correct messages)
248248
\ ],
249249
\ ale#lsp#message#DidChangeConfiguration(bufnr(''), g:ale_lsp_configuration)
250250

251+
Execute(ale#lsp#message#Diagnostic() should return correct messages):
252+
AssertEqual
253+
\ [
254+
\ 0,
255+
\ 'textDocument/diagnostic',
256+
\ {
257+
\ 'textDocument': {
258+
\ 'uri': ale#path#ToFileURI(g:dir . '/foo/bar.ts'),
259+
\ },
260+
\ }
261+
\ ],
262+
\ ale#lsp#message#Diagnostic(bufnr(''))
263+
251264
Execute(ale#lsp#tsserver_message#Open() should return correct messages):
252265
AssertEqual
253266
\ [

test/lsp/test_lsp_startup.vader

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ Before:
193193
\ 'dynamicRegistration': v:false,
194194
\ 'linkSupport': v:false,
195195
\ },
196+
\ 'diagnostic': {
197+
\ 'dynamicRegistration': v:true,
198+
\ 'relatedDocumentSupport': v:true,
199+
\ },
196200
\ 'publishDiagnostics': {
197201
\ 'relatedInformation': v:true,
198202
\ },

test/lsp/test_other_initialize_message_handling.vader

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Execute(Capabilities should be set up correctly):
9090
\ 'includeText': 0,
9191
\ 'references': 1,
9292
\ 'rename': 1,
93+
\ 'pull_model': 0,
9394
\ 'symbol_search': 1,
9495
\ 'typeDefinition': 0,
9596
\ },
@@ -122,6 +123,7 @@ Execute(Disabled capabilities should be recognised correctly):
122123
\ 'definitionProvider': v:false,
123124
\ 'experimental': {},
124125
\ 'documentHighlightProvider': v:true,
126+
\ 'diagnosticProvider': {},
125127
\ },
126128
\ },
127129
\})
@@ -140,6 +142,7 @@ Execute(Disabled capabilities should be recognised correctly):
140142
\ 'includeText': 0,
141143
\ 'references': 0,
142144
\ 'rename': 0,
145+
\ 'pull_model': 0,
143146
\ 'symbol_search': 0,
144147
\ 'typeDefinition': 0,
145148
\ },
@@ -182,6 +185,9 @@ Execute(Capabilities should be enabled when sent as Dictionaries):
182185
\ 'implementationProvider': {},
183186
\ 'experimental': {},
184187
\ 'documentHighlightProvider': v:true,
188+
\ 'diagnosticProvider': {
189+
\ 'interFileDependencies': v:false,
190+
\ },
185191
\ 'workspaceSymbolProvider': {}
186192
\ },
187193
\ },
@@ -201,6 +207,7 @@ Execute(Capabilities should be enabled when sent as Dictionaries):
201207
\ 'includeText': 1,
202208
\ 'references': 1,
203209
\ 'rename': 1,
210+
\ 'pull_model': 1,
204211
\ 'symbol_search': 1,
205212
\ 'typeDefinition': 1,
206213
\ },

0 commit comments

Comments
 (0)