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