3 # Functions related to handling LSP diagnostics.
5 import './options.vim' as opt
6 import './buffer.vim' as buf
10 # serverDiagnostics: {
11 # lspServer1Id: [diag, diag, diag]
12 # lspServer2Id: [diag, diag, diag]
14 # serverDiagnosticsByLnum: {
15 # lspServer1Id: { [lnum]: [diag, diag diag] },
16 # lspServer2Id: { [lnum]: [diag, diag diag] },
18 # sortedDiagnostics: [lspServer1.diags, ...lspServer2.diags]->sort()
20 var diagsMap: dict<dict<any>> = {}
22 # Initialize the signs and the text property type used for diagnostics.
24 # Signs and their highlight groups used for LSP diagnostics
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'}
35 text: opt.lspOptions.diagSignErrorText,
36 texthl: 'LspDiagSignErrorText',
40 name: 'LspDiagWarning',
41 text: opt.lspOptions.diagSignWarningText,
42 texthl: 'LspDiagSignWarningText',
47 text: opt.lspOptions.diagSignInfoText,
48 texthl: 'LspDiagSignInfoText',
53 text: opt.lspOptions.diagSignHintText,
54 texthl: 'LspDiagSignHintText',
59 # Diag inline highlight groups and text property types
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'}
67 var override = &cursorline
68 && &cursorlineopt =~ '\<line\>\|\<screenline\>\|\<both\>'
70 prop_type_add('LspDiagInlineError',
71 {highlight: 'LspDiagInlineError',
74 prop_type_add('LspDiagInlineWarning',
75 {highlight: 'LspDiagInlineWarning',
78 prop_type_add('LspDiagInlineInfo',
79 {highlight: 'LspDiagInlineInfo',
82 prop_type_add('LspDiagInlineHint',
83 {highlight: 'LspDiagInlineHint',
87 # Diag virtual text highlight groups and text property types
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'},
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})
103 autocmd_add([{group: 'LspOptionsChanged',
106 cmd: 'LspDiagsOptionsChanged()'}])
109 if opt.lspOptions.aleSupport
114 pattern: 'ALEWantResults',
115 cmd: 'AleHook(g:ale_want_results_buffer)'
121 # Sort diagnostics ascending based on line and character offset
122 def SortDiags(diags: list<dict<any>>): list<dict<any>>
123 return diags->sort((a, b) => {
124 var a_start = a.range.start
125 var b_start = b.range.start
126 var linediff = a_start.line - b_start.line
128 return a_start.character - b_start.character
134 # Remove the diagnostics stored for buffer "bnr"
135 export def DiagRemoveFile(bnr: number)
136 if diagsMap->has_key(bnr)
137 diagsMap->remove(bnr)
141 def DiagSevToSignName(severity: number): string
142 var typeMap: list<string> = ['LspDiagError', 'LspDiagWarning',
143 'LspDiagInfo', 'LspDiagHint']
147 return typeMap[severity - 1]
150 def DiagSevToInlineHLName(severity: number): string
151 var typeMap: list<string> = [
152 'LspDiagInlineError',
153 'LspDiagInlineWarning',
158 return 'LspDiagInlineHint'
160 return typeMap[severity - 1]
163 def DiagSevToVirtualTextHLName(severity: number): string
164 var typeMap: list<string> = [
165 'LspDiagVirtualTextError',
166 'LspDiagVirtualTextWarning',
167 'LspDiagVirtualTextInfo',
168 'LspDiagVirtualTextHint'
171 return 'LspDiagVirtualTextHint'
173 return typeMap[severity - 1]
176 def DiagSevToSymbolText(severity: number): string
177 var lspOpts = opt.lspOptions
178 var typeMap: list<string> = [
179 lspOpts.diagSignErrorText,
180 lspOpts.diagSignWarningText,
181 lspOpts.diagSignInfoText,
182 lspOpts.diagSignHintText
185 return lspOpts.diagSignHintText
187 return typeMap[severity - 1]
190 # Remove signs and text properties for diagnostics in buffer
191 def RemoveDiagVisualsForBuffer(bnr: number, all: bool = false)
192 var lspOpts = opt.lspOptions
193 if lspOpts.showDiagWithSign || all
194 # Remove all the existing diagnostic signs
195 sign_unplace('LSPDiag', {buffer: bnr})
198 if lspOpts.showDiagWithVirtualText || all
199 # Remove all the existing virtual text
200 prop_remove({type: 'LspDiagVirtualTextError', bufnr: bnr, all: true})
201 prop_remove({type: 'LspDiagVirtualTextWarning', bufnr: bnr, all: true})
202 prop_remove({type: 'LspDiagVirtualTextInfo', bufnr: bnr, all: true})
203 prop_remove({type: 'LspDiagVirtualTextHint', bufnr: bnr, all: true})
206 if lspOpts.highlightDiagInline || all
207 # Remove all the existing virtual text
208 prop_remove({type: 'LspDiagInlineError', bufnr: bnr, all: true})
209 prop_remove({type: 'LspDiagInlineWarning', bufnr: bnr, all: true})
210 prop_remove({type: 'LspDiagInlineInfo', bufnr: bnr, all: true})
211 prop_remove({type: 'LspDiagInlineHint', bufnr: bnr, all: true})
215 # Refresh the placed diagnostics in buffer "bnr"
216 # This inline signs, inline props, and virtual text diagnostics
217 export def DiagsRefresh(bnr: number, all: bool = false)
218 :silent! bnr->bufload()
220 RemoveDiagVisualsForBuffer(bnr, all)
222 if !diagsMap->has_key(bnr) ||
223 diagsMap[bnr].sortedDiagnostics->empty()
227 # Initialize default/fallback properties for diagnostic virtual text:
228 var diag_align: string = 'above'
229 var diag_wrap: string = 'truncate'
230 var diag_symbol: string = '┌─'
231 var lspOpts = opt.lspOptions
233 if lspOpts.diagVirtualTextAlign == 'below'
235 diag_wrap = 'truncate'
237 elseif lspOpts.diagVirtualTextAlign == 'after'
243 var signs: list<dict<any>> = []
244 var diags: list<dict<any>> = diagsMap[bnr].sortedDiagnostics
246 # TODO: prioritize most important severity if there are multiple
247 # diagnostics from the same line
248 var d_range = diag.range
249 var d_start = d_range.start
250 var d_end = d_range.end
251 var lnum = d_start.line + 1
252 if lspOpts.showDiagWithSign
253 signs->add({id: 0, buffer: bnr, group: 'LSPDiag',
254 lnum: lnum, name: DiagSevToSignName(diag.severity),
255 priority: 10 - diag.severity})
259 if lspOpts.highlightDiagInline
260 prop_add(lnum, util.GetLineByteFromPos(bnr, d_start) + 1,
261 {end_lnum: d_end.line + 1,
262 end_col: util.GetLineByteFromPos(bnr, d_end) + 1,
264 type: DiagSevToInlineHLName(diag.severity)})
267 if lspOpts.showDiagWithVirtualText
270 var symbol: string = diag_symbol
272 if diag_align == 'after'
274 symbol = DiagSevToSymbolText(diag.severity)
276 var charIdx = util.GetCharIdxWithoutCompChar(bnr, d_start)
279 padding = strdisplaywidth(getline(lnum)[ : charIdx - 1])
283 prop_add(lnum, 0, {bufnr: bnr,
284 type: DiagSevToVirtualTextHLName(diag.severity),
285 text: $'{symbol} {diag.message}',
286 text_align: diag_align,
287 text_wrap: diag_wrap,
288 text_padding_left: padding})
290 catch /E966\|E964/ # Invalid lnum | Invalid col
291 # Diagnostics arrive asynchronous and the document changed while they
292 # wore send. Ignore this as new once will arrive shortly.
296 if lspOpts.showDiagWithSign
297 signs->sign_placelist()
301 # Sends diagnostics to Ale
302 def SendAleDiags(bnr: number, timerid: number)
303 if !diagsMap->has_key(bnr)
307 # Convert to Ale's diagnostics format (:h ale-loclist-format)
308 ale#other_source#ShowResults(bnr, 'lsp',
309 diagsMap[bnr].sortedDiagnostics->mapnew((_, v) => {
310 return {text: v.message,
311 lnum: v.range.start.line + 1,
312 col: util.GetLineByteFromPos(bnr, v.range.start) + 1,
313 end_lnum: v.range.end.line + 1,
314 end_col: util.GetLineByteFromPos(bnr, v.range.end) + 1,
315 type: "EWIH"[v.severity - 1]}
320 # Hook called when Ale wants to retrieve new diagnostics
321 def AleHook(bnr: number)
322 ale#other_source#StartChecking(bnr, 'lsp')
323 timer_start(0, function('SendAleDiags', [bnr]))
326 # New LSP diagnostic messages received from the server for a file.
327 # Update the signs placed in the buffer for this file
328 export def ProcessNewDiags(bnr: number)
329 DiagsUpdateLocList(bnr)
331 var lspOpts = opt.lspOptions
332 if lspOpts.aleSupport
333 SendAleDiags(bnr, -1)
335 elseif !lspOpts.autoHighlightDiags
339 if bnr == -1 || !diagsMap->has_key(bnr)
343 var curmode: string = mode()
344 if curmode == 'i' || curmode == 'R' || curmode == 'Rv'
345 # postpone placing signs in insert mode and replace mode. These will be
346 # placed after the user returns to Normal mode.
347 b:LspDiagsUpdatePending = true
354 # process a diagnostic notification message from the LSP server
355 # Notification: textDocument/publishDiagnostics
356 # Param: PublishDiagnosticsParams
357 export def DiagNotification(lspserver: dict<any>, uri: string, diags_arg: list<dict<any>>): void
358 # Diagnostics are disabled for this server?
359 var diagSupported = lspserver.features->get('diagnostics', true)
364 var fname: string = util.LspUriToFile(uri)
365 var bnr: number = fname->bufnr()
367 # Is this condition possible?
371 var newDiags: list<dict<any>> = diags_arg
373 if lspserver.needOffsetEncoding
374 # Decode the position encoding in all the diags
375 newDiags->map((_, dval) => {
376 lspserver.decodeRange(bnr, dval.range)
381 if lspserver.processDiagHandler != null_function
382 newDiags = lspserver.processDiagHandler(diags_arg)
385 # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here?
386 var lastlnum: number = bnr->getbufinfo()[0].linecount
388 # store the diagnostic for each line separately
389 var diagsByLnum: dict<list<dict<any>>> = {}
391 var diagWithinRange: list<dict<any>> = []
393 var d_start = diag.range.start
394 if d_start.line + 1 > lastlnum
395 # Make sure the line number is a valid buffer line number
396 d_start.line = lastlnum - 1
399 var lnum = d_start.line + 1
400 if !diagsByLnum->has_key(lnum)
401 diagsByLnum[lnum] = []
403 diagsByLnum[lnum]->add(diag)
405 diagWithinRange->add(diag)
408 var serverDiags: dict<list<any>> = diagsMap->has_key(bnr) ?
409 diagsMap[bnr].serverDiagnostics : {}
410 serverDiags[lspserver.id] = diagWithinRange
412 var serverDiagsByLnum: dict<dict<list<any>>> = diagsMap->has_key(bnr) ?
413 diagsMap[bnr].serverDiagnosticsByLnum : {}
414 serverDiagsByLnum[lspserver.id] = diagsByLnum
416 # store the diagnostic for each line separately
417 var joinedServerDiags: list<dict<any>> = []
418 for diags in serverDiags->values()
420 joinedServerDiags->add(diag)
424 var sortedDiags = SortDiags(joinedServerDiags)
427 sortedDiagnostics: sortedDiags,
428 serverDiagnosticsByLnum: serverDiagsByLnum,
429 serverDiagnostics: serverDiags
434 # Notify user scripts that diags has been updated
435 if exists('#User#LspDiagsUpdated')
436 :doautocmd <nomodeline> User LspDiagsUpdated
440 # get the count of error in the current buffer
441 export def DiagsGetErrorCount(): dict<number>
447 var bnr: number = bufnr()
448 if diagsMap->has_key(bnr)
449 var diags = diagsMap[bnr].sortedDiagnostics
451 var severity = diag->get('severity', -1)
464 return {Error: errCount, Warn: warnCount, Info: infoCount, Hint: hintCount}
467 # Map the LSP DiagnosticSeverity to a quickfix type character
468 def DiagSevToQfType(severity: number): string
469 var typeMap: list<string> = ['E', 'W', 'I', 'N']
475 return typeMap[severity - 1]
478 # Update the location list window for the current window with the diagnostic
480 # Returns true if diagnostics is not empty and false if it is empty.
481 def DiagsUpdateLocList(bnr: number, calledByCmd: bool = false): bool
482 var fname: string = bnr->bufname()->fnamemodify(':p')
487 var LspQfId: number = bnr->getbufvar('LspQfId', 0)
488 if LspQfId == 0 && !opt.lspOptions.autoPopulateDiags && !calledByCmd
489 # Diags location list is not present. Create the location list only if
490 # the 'autoPopulateDiags' option is set or the ":LspDiag show" command is
495 if LspQfId != 0 && getloclist(0, {id: LspQfId}).id != LspQfId
496 # Previously used location list for the diagnostics is gone
500 if !diagsMap->has_key(bnr) ||
501 diagsMap[bnr].sortedDiagnostics->empty()
503 setloclist(0, [], 'r', {id: LspQfId, items: []})
508 var qflist: list<dict<any>> = []
511 var diags = diagsMap[bnr].sortedDiagnostics
513 var d_range = diag.range
514 var d_start = d_range.start
515 var d_end = d_range.end
516 text = diag.message->substitute("\n\\+", "\n", 'g')
517 qflist->add({filename: fname,
518 lnum: d_start.line + 1,
519 col: util.GetLineByteFromPos(bnr, d_start) + 1,
520 end_lnum: d_end.line + 1,
521 end_col: util.GetLineByteFromPos(bnr, d_end) + 1,
523 type: DiagSevToQfType(diag.severity)})
527 var props = {title: 'Language Server Diagnostics', items: qflist}
532 setloclist(0, [], op, props)
534 setbufvar(bnr, 'LspQfId', getloclist(0, {id: 0}).id)
540 # Display the diagnostic messages from the LSP server for the current buffer
542 export def ShowAllDiags(): void
543 if !DiagsUpdateLocList(bufnr(), true)
544 util.WarnMsg($'No diagnostic messages found for {@%}')
548 var save_winid = win_getid()
549 # make the diagnostics error list the active one and open it
550 var LspQfId: number = getbufvar(bufnr(), 'LspQfId', 0)
551 var LspQfNr: number = getloclist(0, {id: LspQfId, nr: 0}).nr
552 exe $':{LspQfNr} lhistory'
554 if !opt.lspOptions.keepFocusInDiags
555 save_winid->win_gotoid()
559 # Display the message of "diag" in a popup window right below the position in
560 # the diagnostic message.
561 def ShowDiagInPopup(diag: dict<any>)
562 var d_start = diag.range.start
563 var dlnum = d_start.line + 1
564 var ltext = dlnum->getline()
565 var dlcol = ltext->byteidxcomp(d_start.character) + 1
567 var lastline = line('$')
569 # The line number is outside the last line in the file.
573 # The column is outside the last character in line.
574 dlcol = ltext->len() + 1
576 var d = screenpos(0, dlnum, dlcol)
578 # If the diag position cannot be converted to Vim lnum/col, then use
579 # the current cursor position
580 d = {row: line('.'), col: col('.')}
583 # Display a popup right below the diagnostics position
584 var msg = diag.message->split("\n")
585 var msglen = msg->reduce((acc, val) => max([acc, val->strcharlen()]), 0)
588 ppopts.pos = 'topleft'
589 ppopts.line = d.row + 1
600 popup_create(msg, ppopts)
603 # Display the "diag" message in a popup or in the status message area
604 def DisplayDiag(diag: dict<any>)
605 if opt.lspOptions.showDiagInPopup
606 # Display the diagnostic message in a popup window.
607 ShowDiagInPopup(diag)
609 # Display the diagnostic message in the status message area
614 # Show the diagnostic message for the current line
615 export def ShowCurrentDiag(atPos: bool)
616 var bnr: number = bufnr()
617 var lnum: number = line('.')
618 var col: number = charcol('.')
619 var diag: dict<any> = GetDiagByPos(bnr, lnum, col, atPos)
621 util.WarnMsg($'No diagnostic messages found for current {atPos ? "position" : "line"}')
627 # Show the diagnostic message for the current line without linebreak
628 export def ShowCurrentDiagInStatusLine()
629 var bnr: number = bufnr()
630 var lnum: number = line('.')
631 var col: number = charcol('.')
632 var diag: dict<any> = GetDiagByPos(bnr, lnum, col)
634 # 15 is a enough length not to cause line break
635 var max_width = &columns - 15
637 if diag->has_key('code')
638 code = $'[{diag.code}] '
640 var msgNoLineBreak = code .. substitute(substitute(diag.message, "\n", ' ', ''), "\\n", ' ', '')
641 :echo msgNoLineBreak[ : max_width]
647 # Get the diagnostic from the LSP server for a particular line and character
649 export def GetDiagByPos(bnr: number, lnum: number, col: number,
650 atPos: bool = false): dict<any>
651 var diags_in_line = GetDiagsByLine(bnr, lnum)
653 for diag in diags_in_line
655 var startCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.start)
656 var endCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.end)
658 if col >= startCharIdx + 1 && col < endCharIdx + 1
661 elseif col <= startCharIdx + 1
666 # No diagnostic to the right of the position, return the last one instead
667 if !atPos && diags_in_line->len() > 0
668 return diags_in_line[-1]
674 # Get all diagnostics from the LSP server for a particular line in a file
675 export def GetDiagsByLine(bnr: number, lnum: number, lspserver: dict<any> = null_dict): list<dict<any>>
676 if !diagsMap->has_key(bnr)
680 var diags: list<dict<any>> = []
682 var serverDiagsByLnum = diagsMap[bnr].serverDiagnosticsByLnum
684 if lspserver == null_dict
685 for diagsByLnum in serverDiagsByLnum->values()
686 if diagsByLnum->has_key(lnum)
687 diags->extend(diagsByLnum[lnum])
691 if !serverDiagsByLnum->has_key(lspserver.id)
694 if serverDiagsByLnum[lspserver.id]->has_key(lnum)
695 diags = serverDiagsByLnum[lspserver.id][lnum]
699 return diags->sort((a, b) => {
700 return a.range.start.character - b.range.start.character
704 # Utility function to do the actual jump
705 def JumpDiag(diag: dict<any>)
706 var startPos: dict<number> = diag.range.start
707 setcursorcharpos(startPos.line + 1,
708 util.GetCharIdxWithoutCompChar(bufnr(), startPos) + 1)
710 if !opt.lspOptions.showDiagWithVirtualText
716 # jump to the next/previous/first diagnostic message in the current buffer
717 export def LspDiagsJump(which: string, a_count: number = 0): void
718 var fname: string = expand('%:p')
722 var bnr: number = bufnr()
724 if !diagsMap->has_key(bnr) ||
725 diagsMap[bnr].sortedDiagnostics->empty()
726 util.WarnMsg($'No diagnostic messages found for {fname}')
730 var diags = diagsMap[bnr].sortedDiagnostics
742 # Find the entry just before the current line (binary search)
743 var count = a_count > 1 ? a_count : 1
744 var curlnum: number = line('.')
745 var curcol: number = charcol('.')
746 for diag in (which == 'next' || which == 'here') ?
747 diags : diags->copy()->reverse()
748 var d_start = diag.range.start
749 var lnum = d_start.line + 1
750 var col = util.GetCharIdxWithoutCompChar(bnr, d_start) + 1
751 if (which == 'next' && (lnum > curlnum || lnum == curlnum && col > curcol))
752 || (which == 'prev' && (lnum < curlnum || lnum == curlnum
754 || (which == 'here' && (lnum == curlnum && col >= curcol))
756 # Skip over as many diags as "count" dictates
767 # If [count] exceeded the remaining diags
768 if which == 'next' && a_count > 1 && a_count != count
773 # If [count] exceeded the previous diags
774 if which == 'prev' && a_count > 1 && a_count != count
780 util.WarnMsg('No more diagnostics found on this line')
782 util.WarnMsg('No more diagnostics found')
786 # Return the sorted diagnostics for buffer "bnr". Default is the current
787 # buffer. A copy of the diagnostics is returned so that the caller can modify
789 export def GetDiagsForBuf(bnr: number = bufnr()): list<dict<any>>
790 if !diagsMap->has_key(bnr) ||
791 diagsMap[bnr].sortedDiagnostics->empty()
795 return diagsMap[bnr].sortedDiagnostics->deepcopy()
798 # Track the current diagnostics auto highlight enabled/disabled state. Used
799 # when the "autoHighlightDiags" option value is changed.
800 var save_autoHighlightDiags = opt.lspOptions.autoHighlightDiags
801 var save_highlightDiagInline = opt.lspOptions.highlightDiagInline
802 var save_showDiagWithSign = opt.lspOptions.showDiagWithSign
803 var save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText
805 # Enable the LSP diagnostics highlighting
806 export def DiagsHighlightEnable()
807 opt.lspOptions.autoHighlightDiags = true
808 save_autoHighlightDiags = true
809 for binfo in getbufinfo({bufloaded: true})
810 if diagsMap->has_key(binfo.bufnr)
811 DiagsRefresh(binfo.bufnr)
816 # Disable the LSP diagnostics highlighting in all the buffers
817 export def DiagsHighlightDisable()
818 # turn off all diags highlight
819 opt.lspOptions.autoHighlightDiags = false
820 save_autoHighlightDiags = false
821 for binfo in getbufinfo()
822 if diagsMap->has_key(binfo.bufnr)
823 RemoveDiagVisualsForBuffer(binfo.bufnr)
828 # Some options are changed. If 'autoHighlightDiags' option is changed, then
829 # either enable or disable diags auto highlight.
830 export def LspDiagsOptionsChanged()
831 if save_autoHighlightDiags && !opt.lspOptions.autoHighlightDiags
832 DiagsHighlightDisable()
833 elseif !save_autoHighlightDiags && opt.lspOptions.autoHighlightDiags
834 DiagsHighlightEnable()
837 if save_highlightDiagInline != opt.lspOptions.highlightDiagInline
838 || save_showDiagWithSign != opt.lspOptions.showDiagWithSign
839 || save_showDiagWithVirtualText != opt.lspOptions.showDiagWithVirtualText
840 save_highlightDiagInline = opt.lspOptions.highlightDiagInline
841 save_showDiagWithSign = opt.lspOptions.showDiagWithSign
842 save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText
843 for binfo in getbufinfo({bufloaded: true})
844 if diagsMap->has_key(binfo.bufnr)
845 DiagsRefresh(binfo.bufnr, true)
851 # vim: tabstop=8 shiftwidth=2 softtabstop=2