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