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