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