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