]> Sergey Matveev's repositories - vim-lsp.git/blob - autoload/lsp/diag.vim
a17598abe6cd03d5ad1b5d3e5d6ef2b753eb666f
[vim-lsp.git] / autoload / lsp / diag.vim
1 vim9script
2
3 # Functions related to handling LSP diagnostics.
4
5 import './options.vim' as opt
6 import './buffer.vim' as buf
7 import './util.vim'
8
9 # [bnr] = {
10 #   serverDiagnostics: {
11 #     lspServer1Id: [diag, diag, diag]
12 #     lspServer2Id: [diag, diag, diag]
13 #   },
14 #   serverDiagnosticsByLnum: {
15 #     lspServer1Id: { [lnum]: [diag, diag diag] },
16 #     lspServer2Id: { [lnum]: [diag, diag diag] },
17 #   },
18 #   sortedDiagnostics: [lspServer1.diags, ...lspServer2.diags]->sort()
19 # }
20 var diagsMap: dict<dict<any>> = {}
21
22 # Initialize the signs and the text property type used for diagnostics.
23 export def InitOnce()
24   # Signs and their highlight groups used for LSP diagnostics
25   hlset([
26     {name: 'LspDiagLine', default: true, linksto: 'DiffAdd'},
27     {name: 'LspDiagSignErrorText', default: true, linksto: 'ErrorMsg'},
28     {name: 'LspDiagSignWarningText', default: true, linksto: 'Search'},
29     {name: 'LspDiagSignInfoText', default: true, linksto: 'Pmenu'},
30     {name: 'LspDiagSignHintText', default: true, linksto: 'Question'}
31   ])
32   sign_define([
33     {
34       name: 'LspDiagError',
35       text: opt.lspOptions.diagSignErrorText,
36       texthl: 'LspDiagSignErrorText',
37       linehl: 'LspDiagLine'
38     },
39     {
40       name: 'LspDiagWarning',
41       text: opt.lspOptions.diagSignWarningText,
42       texthl: 'LspDiagSignWarningText',
43       linehl: 'LspDiagLine'
44     },
45     {
46       name: 'LspDiagInfo',
47       text: opt.lspOptions.diagSignInfoText,
48       texthl: 'LspDiagSignInfoText',
49       linehl: 'LspDiagLine'
50     },
51     {
52       name: 'LspDiagHint',
53       text: opt.lspOptions.diagSignHintText,
54       texthl: 'LspDiagSignHintText',
55       linehl: 'LspDiagLine'
56     }
57   ])
58
59   # Diag inline highlight groups and text property types
60   hlset([
61     {name: 'LspDiagInlineError', default: true, linksto: 'SpellBad'},
62     {name: 'LspDiagInlineWarning', default: true, linksto: 'SpellCap'},
63     {name: 'LspDiagInlineInfo', default: true, linksto: 'SpellRare'},
64     {name: 'LspDiagInlineHint', default: true, linksto: 'SpellLocal'}
65   ])
66
67   var override = &cursorline
68       && &cursorlineopt =~ '\<line\>\|\<screenline\>\|\<both\>'
69
70   prop_type_add('LspDiagInlineError',
71                 {highlight: 'LspDiagInlineError',
72                  priority: 10,
73                  override: override})
74   prop_type_add('LspDiagInlineWarning',
75                 {highlight: 'LspDiagInlineWarning',
76                  priority: 9,
77                  override: override})
78   prop_type_add('LspDiagInlineInfo',
79                 {highlight: 'LspDiagInlineInfo',
80                  priority: 8,
81                  override: override})
82   prop_type_add('LspDiagInlineHint',
83                 {highlight: 'LspDiagInlineHint',
84                  priority: 7,
85                  override: override})
86
87   # Diag virtual text highlight groups and text property types
88   hlset([
89     {name: 'LspDiagVirtualTextError', default: true, linksto: 'SpellBad'},
90     {name: 'LspDiagVirtualTextWarning', default: true, linksto: 'SpellCap'},
91     {name: 'LspDiagVirtualTextInfo', default: true, linksto: 'SpellRare'},
92     {name: 'LspDiagVirtualTextHint', default: true, linksto: 'SpellLocal'},
93   ])
94   prop_type_add('LspDiagVirtualTextError',
95                 {highlight: 'LspDiagVirtualTextError', override: true})
96   prop_type_add('LspDiagVirtualTextWarning',
97                 {highlight: 'LspDiagVirtualTextWarning', override: true})
98   prop_type_add('LspDiagVirtualTextInfo',
99                 {highlight: 'LspDiagVirtualTextInfo', override: true})
100   prop_type_add('LspDiagVirtualTextHint',
101                 {highlight: 'LspDiagVirtualTextHint', override: true})
102
103   autocmd_add([{group: 'LspOptionsChanged',
104                 event: 'User',
105                 pattern: '*',
106                 cmd: 'LspDiagsOptionsChanged()'}])
107
108   # ALE plugin support
109   if opt.lspOptions.aleSupport
110     autocmd_add([
111       {
112         group: 'LspAleCmds',
113         event: 'User',
114         pattern: 'ALEWantResults',
115         cmd: 'AleHook(g:ale_want_results_buffer)'
116       }
117     ])
118   endif
119 enddef
120
121 # Sort diagnostics ascending based on line and character offset
122 def SortDiags(diags: list<dict<any>>): list<dict<any>>
123   return diags->sort((a, b) => {
124     var a_start = a.range.start
125     var b_start = b.range.start
126     var linediff = a_start.line - b_start.line
127     if linediff == 0
128       return a_start.character - b_start.character
129     endif
130     return linediff
131   })
132 enddef
133
134 # Remove the diagnostics stored for buffer "bnr"
135 export def DiagRemoveFile(bnr: number)
136   if diagsMap->has_key(bnr)
137     diagsMap->remove(bnr)
138   endif
139 enddef
140
141 def DiagSevToSignName(severity: number): string
142   var typeMap: list<string> = ['LspDiagError', 'LspDiagWarning',
143                                                 'LspDiagInfo', 'LspDiagHint']
144   if severity > 4
145     return 'LspDiagHint'
146   endif
147   return typeMap[severity - 1]
148 enddef
149
150 def DiagSevToInlineHLName(severity: number): string
151   var typeMap: list<string> = [
152     'LspDiagInlineError',
153     'LspDiagInlineWarning',
154     'LspDiagInlineInfo',
155     'LspDiagInlineHint'
156   ]
157   if severity > 4
158     return 'LspDiagInlineHint'
159   endif
160   return typeMap[severity - 1]
161 enddef
162
163 def DiagSevToVirtualTextHLName(severity: number): string
164   var typeMap: list<string> = [
165     'LspDiagVirtualTextError',
166     'LspDiagVirtualTextWarning',
167     'LspDiagVirtualTextInfo',
168     'LspDiagVirtualTextHint'
169   ]
170   if severity > 4
171     return 'LspDiagVirtualTextHint'
172   endif
173   return typeMap[severity - 1]
174 enddef
175
176 def DiagSevToSymbolText(severity: number): string
177   var lspOpts = opt.lspOptions
178   var typeMap: list<string> = [
179     lspOpts.diagSignErrorText,
180     lspOpts.diagSignWarningText,
181     lspOpts.diagSignInfoText,
182     lspOpts.diagSignHintText
183   ]
184   if severity > 4
185     return lspOpts.diagSignHintText
186   endif
187   return typeMap[severity - 1]
188 enddef
189
190 # Remove signs and text properties for diagnostics in buffer
191 def RemoveDiagVisualsForBuffer(bnr: number, all: bool = false)
192   var lspOpts = opt.lspOptions
193   if lspOpts.showDiagWithSign || all
194     # Remove all the existing diagnostic signs
195     sign_unplace('LSPDiag', {buffer: bnr})
196   endif
197
198   if lspOpts.showDiagWithVirtualText || all
199     # Remove all the existing virtual text
200     prop_remove({type: 'LspDiagVirtualTextError', bufnr: bnr, all: true})
201     prop_remove({type: 'LspDiagVirtualTextWarning', bufnr: bnr, all: true})
202     prop_remove({type: 'LspDiagVirtualTextInfo', bufnr: bnr, all: true})
203     prop_remove({type: 'LspDiagVirtualTextHint', bufnr: bnr, all: true})
204   endif
205
206   if lspOpts.highlightDiagInline || all
207     # Remove all the existing virtual text
208     prop_remove({type: 'LspDiagInlineError', bufnr: bnr, all: true})
209     prop_remove({type: 'LspDiagInlineWarning', bufnr: bnr, all: true})
210     prop_remove({type: 'LspDiagInlineInfo', bufnr: bnr, all: true})
211     prop_remove({type: 'LspDiagInlineHint', bufnr: bnr, all: true})
212   endif
213 enddef
214
215 # Refresh the placed diagnostics in buffer "bnr"
216 # This inline signs, inline props, and virtual text diagnostics
217 export def DiagsRefresh(bnr: number, all: bool = false)
218   :silent! bnr->bufload()
219
220   RemoveDiagVisualsForBuffer(bnr, all)
221
222   if !diagsMap->has_key(bnr) ||
223       diagsMap[bnr].sortedDiagnostics->empty()
224     return
225   endif
226
227   # Initialize default/fallback properties for diagnostic virtual text:
228   var diag_align: string = 'above'
229   var diag_wrap: string = 'truncate'
230   var diag_symbol: string = '┌─'
231   var lspOpts = opt.lspOptions
232
233   if lspOpts.diagVirtualTextAlign == 'below'
234     diag_align = 'below'
235     diag_wrap = 'truncate'
236     diag_symbol = '└─'
237   elseif lspOpts.diagVirtualTextAlign == 'after'
238     diag_align = 'after'
239     diag_wrap = 'wrap'
240     diag_symbol = 'E>'
241   endif
242
243   var signs: list<dict<any>> = []
244   var diags: list<dict<any>> = diagsMap[bnr].sortedDiagnostics
245   for diag in diags
246     # TODO: prioritize most important severity if there are multiple
247     # diagnostics from the same line
248     var d_range = diag.range
249     var d_start = d_range.start
250     var d_end = d_range.end
251     var lnum = d_start.line + 1
252     if lspOpts.showDiagWithSign
253       signs->add({id: 0, buffer: bnr, group: 'LSPDiag',
254                   lnum: lnum, name: DiagSevToSignName(diag.severity),
255                   priority: 10 - diag.severity})
256     endif
257
258     try
259       if lspOpts.highlightDiagInline
260         prop_add(lnum, util.GetLineByteFromPos(bnr, d_start) + 1,
261                  {end_lnum: d_end.line + 1,
262                   end_col: util.GetLineByteFromPos(bnr, d_end) + 1,
263                   bufnr: bnr,
264                   type: DiagSevToInlineHLName(diag.severity)})
265       endif
266
267       if lspOpts.showDiagWithVirtualText
268
269         var padding: number
270         var symbol: string = diag_symbol
271
272         if diag_align == 'after'
273           padding = 3
274           symbol = DiagSevToSymbolText(diag.severity)
275         else
276           var charIdx = util.GetCharIdxWithoutCompChar(bnr, d_start)
277           padding = charIdx
278           if padding > 0
279             padding = strdisplaywidth(getline(lnum)[ : charIdx - 1])
280           endif
281         endif
282
283         prop_add(lnum, 0, {bufnr: bnr,
284                            type: DiagSevToVirtualTextHLName(diag.severity),
285                            text: $'{symbol} {diag.message}',
286                            text_align: diag_align,
287                            text_wrap: diag_wrap,
288                            text_padding_left: padding})
289       endif
290     catch /E966\|E964/ # Invalid lnum | Invalid col
291       # Diagnostics arrive asynchronous and the document changed while they
292       # wore send. Ignore this as new once will arrive shortly.
293     endtry
294   endfor
295
296   if lspOpts.showDiagWithSign
297     signs->sign_placelist()
298   endif
299 enddef
300
301 # Sends diagnostics to Ale
302 def SendAleDiags(bnr: number, timerid: number)
303   if !diagsMap->has_key(bnr)
304     return
305   endif
306
307   # Convert to Ale's diagnostics format (:h ale-loclist-format)
308   ale#other_source#ShowResults(bnr, 'lsp',
309     diagsMap[bnr].sortedDiagnostics->mapnew((_, v) => {
310      return {text: v.message,
311              lnum: v.range.start.line + 1,
312              col: util.GetLineByteFromPos(bnr, v.range.start) + 1,
313              end_lnum: v.range.end.line + 1,
314              end_col: util.GetLineByteFromPos(bnr, v.range.end) + 1,
315              type: "EWIH"[v.severity - 1]}
316     })
317   )
318 enddef
319
320 # Hook called when Ale wants to retrieve new diagnostics
321 def AleHook(bnr: number)
322   ale#other_source#StartChecking(bnr, 'lsp')
323   timer_start(0, function('SendAleDiags', [bnr]))
324 enddef
325
326 # New LSP diagnostic messages received from the server for a file.
327 # Update the signs placed in the buffer for this file
328 export def ProcessNewDiags(bnr: number)
329   DiagsUpdateLocList(bnr)
330
331   var lspOpts = opt.lspOptions
332   if lspOpts.aleSupport
333     SendAleDiags(bnr, -1)
334     return
335   elseif !lspOpts.autoHighlightDiags
336     return
337   endif
338
339   if bnr == -1 || !diagsMap->has_key(bnr)
340     return
341   endif
342
343   var curmode: string = mode()
344   if curmode == 'i' || curmode == 'R' || curmode == 'Rv'
345     # postpone placing signs in insert mode and replace mode. These will be
346     # placed after the user returns to Normal mode.
347     b:LspDiagsUpdatePending = true
348     return
349   endif
350
351   DiagsRefresh(bnr)
352 enddef
353
354 # process a diagnostic notification message from the LSP server
355 # Notification: textDocument/publishDiagnostics
356 # Param: PublishDiagnosticsParams
357 export def DiagNotification(lspserver: dict<any>, uri: string, diags_arg: list<dict<any>>): void
358   # Diagnostics are disabled for this server?
359   var diagSupported = lspserver.features->get('diagnostics', true)
360   if !diagSupported
361     return
362   endif
363
364   var fname: string = util.LspUriToFile(uri)
365   var bnr: number = fname->bufnr()
366   if bnr == -1
367     # Is this condition possible?
368     return
369   endif
370
371   var newDiags: list<dict<any>> = diags_arg
372
373   if lspserver.needOffsetEncoding
374     # Decode the position encoding in all the diags
375     newDiags->map((_, dval) => {
376         lspserver.decodeRange(bnr, dval.range)
377         return dval
378       })
379   endif
380
381   if lspserver.processDiagHandler != null_function
382     newDiags = lspserver.processDiagHandler(diags_arg)
383   endif
384
385   # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here?
386   var lastlnum: number = bnr->getbufinfo()[0].linecount
387
388   # store the diagnostic for each line separately
389   var diagsByLnum: dict<list<dict<any>>> = {}
390
391   var diagWithinRange: list<dict<any>> = []
392   for diag in newDiags
393     var d_start = diag.range.start
394     if d_start.line + 1 > lastlnum
395       # Make sure the line number is a valid buffer line number
396       d_start.line = lastlnum - 1
397     endif
398
399     var lnum = d_start.line + 1
400     if !diagsByLnum->has_key(lnum)
401       diagsByLnum[lnum] = []
402     endif
403     diagsByLnum[lnum]->add(diag)
404
405     diagWithinRange->add(diag)
406   endfor
407
408   var serverDiags: dict<list<any>> = diagsMap->has_key(bnr) ?
409       diagsMap[bnr].serverDiagnostics : {}
410   serverDiags[lspserver.id] = diagWithinRange
411
412   var serverDiagsByLnum: dict<dict<list<any>>> = diagsMap->has_key(bnr) ?
413       diagsMap[bnr].serverDiagnosticsByLnum : {}
414   serverDiagsByLnum[lspserver.id] = diagsByLnum
415
416   # store the diagnostic for each line separately
417   var joinedServerDiags: list<dict<any>> = []
418   for diags in serverDiags->values()
419     for diag in diags
420       joinedServerDiags->add(diag)
421     endfor
422   endfor
423
424   var sortedDiags = SortDiags(joinedServerDiags)
425
426   diagsMap[bnr] = {
427     sortedDiagnostics: sortedDiags,
428     serverDiagnosticsByLnum: serverDiagsByLnum,
429     serverDiagnostics: serverDiags
430   }
431
432   ProcessNewDiags(bnr)
433
434   # Notify user scripts that diags has been updated
435   if exists('#User#LspDiagsUpdated')
436     :doautocmd <nomodeline> User LspDiagsUpdated
437   endif
438 enddef
439
440 # get the count of error in the current buffer
441 export def DiagsGetErrorCount(): dict<number>
442   var errCount = 0
443   var warnCount = 0
444   var infoCount = 0
445   var hintCount = 0
446
447   var bnr: number = bufnr()
448   if diagsMap->has_key(bnr)
449     var diags = diagsMap[bnr].sortedDiagnostics
450     for diag in diags
451       var severity = diag->get('severity', -1)
452       if severity == 1
453         errCount += 1
454       elseif severity == 2
455         warnCount += 1
456       elseif severity == 3
457         infoCount += 1
458       elseif severity == 4
459         hintCount += 1
460       endif
461     endfor
462   endif
463
464   return {Error: errCount, Warn: warnCount, Info: infoCount, Hint: hintCount}
465 enddef
466
467 # Map the LSP DiagnosticSeverity to a quickfix type character
468 def DiagSevToQfType(severity: number): string
469   var typeMap: list<string> = ['E', 'W', 'I', 'N']
470
471   if severity > 4
472     return ''
473   endif
474
475   return typeMap[severity - 1]
476 enddef
477
478 # Update the location list window for the current window with the diagnostic
479 # messages.
480 # Returns true if diagnostics is not empty and false if it is empty.
481 def DiagsUpdateLocList(bnr: number, calledByCmd: bool = false): bool
482   var fname: string = bnr->bufname()->fnamemodify(':p')
483   if fname->empty()
484     return false
485   endif
486
487   var LspQfId: number = bnr->getbufvar('LspQfId', 0)
488   if LspQfId == 0 && !opt.lspOptions.autoPopulateDiags && !calledByCmd
489     # Diags location list is not present. Create the location list only if
490     # the 'autoPopulateDiags' option is set or the ":LspDiag show" command is
491     # invoked.
492     return false
493   endif
494
495   if LspQfId != 0 && getloclist(0, {id: LspQfId}).id != LspQfId
496     # Previously used location list for the diagnostics is gone
497     LspQfId = 0
498   endif
499
500   if !diagsMap->has_key(bnr) ||
501       diagsMap[bnr].sortedDiagnostics->empty()
502     if LspQfId != 0
503       setloclist(0, [], 'r', {id: LspQfId, items: []})
504     endif
505     return false
506   endif
507
508   var qflist: list<dict<any>> = []
509   var text: string
510
511   var diags = diagsMap[bnr].sortedDiagnostics
512   for diag in diags
513     var d_range = diag.range
514     var d_start = d_range.start
515     var d_end = d_range.end
516     text = diag.message->substitute("\n\\+", "\n", 'g')
517     qflist->add({filename: fname,
518                     lnum: d_start.line + 1,
519                     col: util.GetLineByteFromPos(bnr, d_start) + 1,
520                     end_lnum: d_end.line + 1,
521                     end_col: util.GetLineByteFromPos(bnr, d_end) + 1,
522                     text: text,
523                     type: DiagSevToQfType(diag.severity)})
524   endfor
525
526   var op: string = ' '
527   var props = {title: 'Language Server Diagnostics', items: qflist}
528   if LspQfId != 0
529     op = 'r'
530     props.id = LspQfId
531   endif
532   setloclist(0, [], op, props)
533   if LspQfId == 0
534     setbufvar(bnr, 'LspQfId', getloclist(0, {id: 0}).id)
535   endif
536
537   return true
538 enddef
539
540 # Display the diagnostic messages from the LSP server for the current buffer
541 # in a location list
542 export def ShowAllDiags(): void
543   if !DiagsUpdateLocList(bufnr(), true)
544     util.WarnMsg($'No diagnostic messages found for {@%}')
545     return
546   endif
547
548   var save_winid = win_getid()
549   # make the diagnostics error list the active one and open it
550   var LspQfId: number = getbufvar(bufnr(), 'LspQfId', 0)
551   var LspQfNr: number = getloclist(0, {id: LspQfId, nr: 0}).nr
552   exe $':{LspQfNr} lhistory'
553   :lopen
554   if !opt.lspOptions.keepFocusInDiags
555     save_winid->win_gotoid()
556   endif
557 enddef
558
559 # Display the message of "diag" in a popup window right below the position in
560 # the diagnostic message.
561 def ShowDiagInPopup(diag: dict<any>)
562   var d_start = diag.range.start
563   var dlnum = d_start.line + 1
564   var ltext = dlnum->getline()
565   var dlcol = ltext->byteidxcomp(d_start.character) + 1
566
567   var lastline = line('$')
568   if dlnum > lastline
569     # The line number is outside the last line in the file.
570     dlnum = lastline
571   endif
572   if dlcol < 1
573     # The column is outside the last character in line.
574     dlcol = ltext->len() + 1
575   endif
576   var d = screenpos(0, dlnum, dlcol)
577   if d->empty()
578     # If the diag position cannot be converted to Vim lnum/col, then use
579     # the current cursor position
580     d = {row: line('.'), col: col('.')}
581   endif
582
583   # Display a popup right below the diagnostics position
584   var msg = diag.message->split("\n")
585   var msglen = msg->reduce((acc, val) => max([acc, val->strcharlen()]), 0)
586
587   var ppopts = {}
588   ppopts.pos = 'topleft'
589   ppopts.line = d.row + 1
590   ppopts.moved = 'any'
591
592   if msglen > &columns
593     ppopts.wrap = true
594     ppopts.col = 1
595   else
596     ppopts.wrap = false
597     ppopts.col = d.col
598   endif
599
600   popup_create(msg, ppopts)
601 enddef
602
603 # Display the "diag" message in a popup or in the status message area
604 def DisplayDiag(diag: dict<any>)
605   if opt.lspOptions.showDiagInPopup
606     # Display the diagnostic message in a popup window.
607     ShowDiagInPopup(diag)
608   else
609     # Display the diagnostic message in the status message area
610     :echo diag.message
611   endif
612 enddef
613
614 # Show the diagnostic message for the current line
615 export def ShowCurrentDiag(atPos: bool)
616   var bnr: number = bufnr()
617   var lnum: number = line('.')
618   var col: number = charcol('.')
619   var diag: dict<any> = GetDiagByPos(bnr, lnum, col, atPos)
620   if diag->empty()
621     util.WarnMsg($'No diagnostic messages found for current {atPos ? "position" : "line"}')
622   else
623     DisplayDiag(diag)
624   endif
625 enddef
626
627 # Show the diagnostic message for the current line without linebreak
628 export def ShowCurrentDiagInStatusLine()
629   var bnr: number = bufnr()
630   var lnum: number = line('.')
631   var col: number = charcol('.')
632   var diag: dict<any> = GetDiagByPos(bnr, lnum, col)
633   if !diag->empty()
634     # 15 is a enough length not to cause line break
635     var max_width = &columns - 15
636     var code = ''
637     if diag->has_key('code')
638       code = $'[{diag.code}] '
639     endif
640     var msgNoLineBreak = code .. substitute(substitute(diag.message, "\n", ' ', ''), "\\n", ' ', '')
641     :echo msgNoLineBreak[ : max_width]
642   else
643     :echo ''
644   endif
645 enddef
646
647 # Get the diagnostic from the LSP server for a particular line and character
648 # offset in a file
649 export def GetDiagByPos(bnr: number, lnum: number, col: number,
650                         atPos: bool = false): dict<any>
651   var diags_in_line = GetDiagsByLine(bnr, lnum)
652
653   for diag in diags_in_line
654     var r = diag.range
655     var startCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.start)
656     var endCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.end)
657     if atPos
658       if col >= startCharIdx + 1 && col < endCharIdx + 1
659         return diag
660       endif
661     elseif col <= startCharIdx + 1
662       return diag
663     endif
664   endfor
665
666   # No diagnostic to the right of the position, return the last one instead
667   if !atPos && diags_in_line->len() > 0
668     return diags_in_line[-1]
669   endif
670
671   return {}
672 enddef
673
674 # Get all diagnostics from the LSP server for a particular line in a file
675 export def GetDiagsByLine(bnr: number, lnum: number, lspserver: dict<any> = null_dict): list<dict<any>>
676   if !diagsMap->has_key(bnr)
677     return []
678   endif
679
680   var diags: list<dict<any>> = []
681
682   var serverDiagsByLnum = diagsMap[bnr].serverDiagnosticsByLnum
683
684   if lspserver == null_dict
685     for diagsByLnum in serverDiagsByLnum->values()
686       if diagsByLnum->has_key(lnum)
687         diags->extend(diagsByLnum[lnum])
688       endif
689     endfor
690   else
691     if !serverDiagsByLnum->has_key(lspserver.id)
692       return []
693     endif
694     if serverDiagsByLnum[lspserver.id]->has_key(lnum)
695       diags = serverDiagsByLnum[lspserver.id][lnum]
696     endif
697   endif
698
699   return diags->sort((a, b) => {
700     return a.range.start.character - b.range.start.character
701   })
702 enddef
703
704 # Utility function to do the actual jump
705 def JumpDiag(diag: dict<any>)
706   var startPos: dict<number> = diag.range.start
707   setcursorcharpos(startPos.line + 1,
708                    util.GetCharIdxWithoutCompChar(bufnr(), startPos) + 1)
709   :normal! zv
710   if !opt.lspOptions.showDiagWithVirtualText
711     :redraw
712     DisplayDiag(diag)
713   endif
714 enddef
715
716 # jump to the next/previous/first diagnostic message in the current buffer
717 export def LspDiagsJump(which: string, a_count: number = 0): void
718   var fname: string = expand('%:p')
719   if fname->empty()
720     return
721   endif
722   var bnr: number = bufnr()
723
724   if !diagsMap->has_key(bnr) ||
725       diagsMap[bnr].sortedDiagnostics->empty()
726     util.WarnMsg($'No diagnostic messages found for {fname}')
727     return
728   endif
729
730   var diags = diagsMap[bnr].sortedDiagnostics
731
732   if which == 'first'
733     JumpDiag(diags[0])
734     return
735   endif
736
737   if which == 'last'
738     JumpDiag(diags[-1])
739     return
740   endif
741
742   # Find the entry just before the current line (binary search)
743   var count = a_count > 1 ? a_count : 1
744   var curlnum: number = line('.')
745   var curcol: number = charcol('.')
746   for diag in (which == 'next' || which == 'here') ?
747                                         diags : diags->copy()->reverse()
748     var d_start = diag.range.start
749     var lnum = d_start.line + 1
750     var col = util.GetCharIdxWithoutCompChar(bnr, d_start) + 1
751     if (which == 'next' && (lnum > curlnum || lnum == curlnum && col > curcol))
752           || (which == 'prev' && (lnum < curlnum || lnum == curlnum
753                                                         && col < curcol))
754           || (which == 'here' && (lnum == curlnum && col >= curcol))
755
756       # Skip over as many diags as "count" dictates
757       count = count - 1
758       if count > 0
759         continue
760       endif
761
762       JumpDiag(diag)
763       return
764     endif
765   endfor
766
767   # If [count] exceeded the remaining diags
768   if which == 'next' && a_count > 1 && a_count != count
769     JumpDiag(diags[-1])
770     return
771   endif
772
773   # If [count] exceeded the previous diags
774   if which == 'prev' && a_count > 1 && a_count != count
775     JumpDiag(diags[0])
776     return
777   endif
778
779   if which == 'here'
780     util.WarnMsg('No more diagnostics found on this line')
781   else
782     util.WarnMsg('No more diagnostics found')
783   endif
784 enddef
785
786 # Return the sorted diagnostics for buffer "bnr".  Default is the current
787 # buffer.  A copy of the diagnostics is returned so that the caller can modify
788 # the diagnostics.
789 export def GetDiagsForBuf(bnr: number = bufnr()): list<dict<any>>
790   if !diagsMap->has_key(bnr) ||
791       diagsMap[bnr].sortedDiagnostics->empty()
792     return []
793   endif
794
795   return diagsMap[bnr].sortedDiagnostics->deepcopy()
796 enddef
797
798 # Track the current diagnostics auto highlight enabled/disabled state.  Used
799 # when the "autoHighlightDiags" option value is changed.
800 var save_autoHighlightDiags = opt.lspOptions.autoHighlightDiags
801 var save_highlightDiagInline = opt.lspOptions.highlightDiagInline
802 var save_showDiagWithSign = opt.lspOptions.showDiagWithSign
803 var save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText
804
805 # Enable the LSP diagnostics highlighting
806 export def DiagsHighlightEnable()
807   opt.lspOptions.autoHighlightDiags = true
808   save_autoHighlightDiags = true
809   for binfo in getbufinfo({bufloaded: true})
810     if diagsMap->has_key(binfo.bufnr)
811       DiagsRefresh(binfo.bufnr)
812     endif
813   endfor
814 enddef
815
816 # Disable the LSP diagnostics highlighting in all the buffers
817 export def DiagsHighlightDisable()
818   # turn off all diags highlight
819   opt.lspOptions.autoHighlightDiags = false
820   save_autoHighlightDiags = false
821   for binfo in getbufinfo()
822     if diagsMap->has_key(binfo.bufnr)
823       RemoveDiagVisualsForBuffer(binfo.bufnr)
824     endif
825   endfor
826 enddef
827
828 # Some options are changed.  If 'autoHighlightDiags' option is changed, then
829 # either enable or disable diags auto highlight.
830 export def LspDiagsOptionsChanged()
831   if save_autoHighlightDiags && !opt.lspOptions.autoHighlightDiags
832     DiagsHighlightDisable()
833   elseif !save_autoHighlightDiags && opt.lspOptions.autoHighlightDiags
834     DiagsHighlightEnable()
835   endif
836
837   if save_highlightDiagInline != opt.lspOptions.highlightDiagInline
838     || save_showDiagWithSign != opt.lspOptions.showDiagWithSign
839     || save_showDiagWithVirtualText != opt.lspOptions.showDiagWithVirtualText
840     save_highlightDiagInline = opt.lspOptions.highlightDiagInline
841     save_showDiagWithSign = opt.lspOptions.showDiagWithSign
842     save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText
843     for binfo in getbufinfo({bufloaded: true})
844       if diagsMap->has_key(binfo.bufnr)
845         DiagsRefresh(binfo.bufnr, true)
846       endif
847     endfor
848   endif
849 enddef
850
851 # vim: tabstop=8 shiftwidth=2 softtabstop=2