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