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
110 opt.lspOptions.autoHighlightDiags = false
115 pattern: 'ALEWantResults',
116 cmd: 'AleHook(g:ale_want_results_buffer)'
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
129 return a_start.character - b_start.character
135 # Remove the diagnostics stored for buffer "bnr"
136 export def DiagRemoveFile(bnr: number)
137 if diagsMap->has_key(bnr)
138 diagsMap->remove(bnr)
142 def DiagSevToSignName(severity: number): string
143 var typeMap: list<string> = ['LspDiagError', 'LspDiagWarning',
144 'LspDiagInfo', 'LspDiagHint']
148 return typeMap[severity - 1]
151 def DiagSevToInlineHLName(severity: number): string
152 var typeMap: list<string> = [
153 'LspDiagInlineError',
154 'LspDiagInlineWarning',
159 return 'LspDiagInlineHint'
161 return typeMap[severity - 1]
164 def DiagSevToVirtualTextHLName(severity: number): string
165 var typeMap: list<string> = [
166 'LspDiagVirtualTextError',
167 'LspDiagVirtualTextWarning',
168 'LspDiagVirtualTextInfo',
169 'LspDiagVirtualTextHint'
172 return 'LspDiagVirtualTextHint'
174 return typeMap[severity - 1]
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
186 return lspOpts.diagSignHintText
188 return typeMap[severity - 1]
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})
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})
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})
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
224 :silent! bnr->bufload()
226 RemoveDiagVisualsForBuffer(bnr, all)
228 if !diagsMap->has_key(bnr) ||
229 diagsMap[bnr].sortedDiagnostics->empty()
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 = '┌─'
238 if lspOpts.diagVirtualTextAlign == 'below'
240 diag_wrap = 'truncate'
242 elseif lspOpts.diagVirtualTextAlign == 'after'
248 var signs: list<dict<any>> = []
249 var diags: list<dict<any>> = diagsMap[bnr].sortedDiagnostics
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})
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,
269 type: DiagSevToInlineHLName(diag.severity)})
272 if lspOpts.showDiagWithVirtualText
275 var symbol: string = diag_symbol
277 if diag_align == 'after'
279 symbol = DiagSevToSymbolText(diag.severity)
281 var charIdx = util.GetCharIdxWithoutCompChar(bnr, d_start)
284 padding = strdisplaywidth(getline(lnum)[ : charIdx - 1])
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})
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.
301 if lspOpts.showDiagWithSign
302 signs->sign_placelist()
306 # Sends diagnostics to Ale
307 def SendAleDiags(bnr: number, timerid: number)
308 if !diagsMap->has_key(bnr)
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]}
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]))
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)
336 var lspOpts = opt.lspOptions
337 if lspOpts.aleSupport
338 SendAleDiags(bnr, -1)
341 if bnr == -1 || !diagsMap->has_key(bnr)
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
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)
366 var fname: string = util.LspUriToFile(uri)
367 var bnr: number = fname->bufnr()
369 # Is this condition possible?
373 var newDiags: list<dict<any>> = diags_arg
375 if lspserver.needOffsetEncoding
376 # Decode the position encoding in all the diags
377 newDiags->map((_, dval) => {
378 lspserver.decodeRange(bnr, dval.range)
383 if lspserver.processDiagHandler != null_function
384 newDiags = lspserver.processDiagHandler(diags_arg)
387 # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here?
388 var lastlnum: number = bnr->getbufinfo()[0].linecount
390 # store the diagnostic for each line separately
391 var diagsByLnum: dict<list<dict<any>>> = {}
393 var diagWithinRange: list<dict<any>> = []
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
401 var lnum = d_start.line + 1
402 if !diagsByLnum->has_key(lnum)
403 diagsByLnum[lnum] = []
405 diagsByLnum[lnum]->add(diag)
407 diagWithinRange->add(diag)
410 var serverDiags: dict<list<any>> = diagsMap->has_key(bnr) ?
411 diagsMap[bnr].serverDiagnostics : {}
412 serverDiags[lspserver.id] = diagWithinRange
414 var serverDiagsByLnum: dict<dict<list<any>>> = diagsMap->has_key(bnr) ?
415 diagsMap[bnr].serverDiagnosticsByLnum : {}
416 serverDiagsByLnum[lspserver.id] = diagsByLnum
418 # store the diagnostic for each line separately
419 var joinedServerDiags: list<dict<any>> = []
420 for diags in serverDiags->values()
422 joinedServerDiags->add(diag)
426 var sortedDiags = SortDiags(joinedServerDiags)
429 sortedDiagnostics: sortedDiags,
430 serverDiagnosticsByLnum: serverDiagsByLnum,
431 serverDiagnostics: serverDiags
436 # Notify user scripts that diags has been updated
437 if exists('#User#LspDiagsUpdated')
438 :doautocmd <nomodeline> User LspDiagsUpdated
442 # get the count of error in the current buffer
443 export def DiagsGetErrorCount(): dict<number>
449 var bnr: number = bufnr()
450 if diagsMap->has_key(bnr)
451 var diags = diagsMap[bnr].sortedDiagnostics
453 var severity = diag->get('severity', -1)
466 return {Error: errCount, Warn: warnCount, Info: infoCount, Hint: hintCount}
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']
477 return typeMap[severity - 1]
480 # Update the location list window for the current window with the diagnostic
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')
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
497 if LspQfId != 0 && getloclist(0, {id: LspQfId}).id != LspQfId
498 # Previously used location list for the diagnostics is gone
502 if !diagsMap->has_key(bnr) ||
503 diagsMap[bnr].sortedDiagnostics->empty()
505 setloclist(0, [], 'r', {id: LspQfId, items: []})
510 var qflist: list<dict<any>> = []
513 var diags = diagsMap[bnr].sortedDiagnostics
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,
525 type: DiagSevToQfType(diag.severity)})
529 var props = {title: 'Language Server Diagnostics', items: qflist}
534 setloclist(0, [], op, props)
536 setbufvar(bnr, 'LspQfId', getloclist(0, {id: 0}).id)
542 # Display the diagnostic messages from the LSP server for the current buffer
544 export def ShowAllDiags(): void
545 if !DiagsUpdateLocList(bufnr(), true)
546 util.WarnMsg($'No diagnostic messages found for {@%}')
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'
556 if !opt.lspOptions.keepFocusInDiags
557 save_winid->win_gotoid()
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
569 var lastline = line('$')
571 # The line number is outside the last line in the file.
575 # The column is outside the last character in line.
576 dlcol = ltext->len() + 1
578 var d = screenpos(0, dlnum, dlcol)
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('.')}
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)
590 ppopts.pos = 'topleft'
591 ppopts.line = d.row + 1
602 popup_create(msg, ppopts)
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)
611 # Display the diagnostic message in the status message area
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)
623 util.WarnMsg($'No diagnostic messages found for current {atPos ? "position" : "line"}')
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)
636 # 15 is a enough length not to cause line break
637 var max_width = &columns - 15
639 if diag->has_key('code')
640 code = $'[{diag.code}] '
642 var msgNoLineBreak = code .. substitute(substitute(diag.message, "\n", ' ', ''), "\\n", ' ', '')
643 :echo msgNoLineBreak[ : max_width]
649 # Get the diagnostic from the LSP server for a particular line and character
651 export def GetDiagByPos(bnr: number, lnum: number, col: number,
652 atPos: bool = false): dict<any>
653 var diags_in_line = GetDiagsByLine(bnr, lnum)
655 for diag in diags_in_line
657 var startCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.start)
658 var endCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.end)
660 if col >= startCharIdx + 1 && col < endCharIdx + 1
663 elseif col <= startCharIdx + 1
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]
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)
682 var diags: list<dict<any>> = []
684 var serverDiagsByLnum = diagsMap[bnr].serverDiagnosticsByLnum
686 if lspserver == null_dict
687 for diagsByLnum in serverDiagsByLnum->values()
688 if diagsByLnum->has_key(lnum)
689 diags->extend(diagsByLnum[lnum])
693 if !serverDiagsByLnum->has_key(lspserver.id)
696 if serverDiagsByLnum[lspserver.id]->has_key(lnum)
697 diags = serverDiagsByLnum[lspserver.id][lnum]
701 return diags->sort((a, b) => {
702 return a.range.start.character - b.range.start.character
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)
712 if !opt.lspOptions.showDiagWithVirtualText
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')
724 var bnr: number = bufnr()
726 if !diagsMap->has_key(bnr) ||
727 diagsMap[bnr].sortedDiagnostics->empty()
728 util.WarnMsg($'No diagnostic messages found for {fname}')
732 var diags = diagsMap[bnr].sortedDiagnostics
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
756 || (which == 'here' && (lnum == curlnum && col >= curcol))
758 # Skip over as many diags as "count" dictates
769 # If [count] exceeded the remaining diags
770 if which == 'next' && a_count > 1 && a_count != count
775 # If [count] exceeded the previous diags
776 if which == 'prev' && a_count > 1 && a_count != count
782 util.WarnMsg('No more diagnostics found on this line')
784 util.WarnMsg('No more diagnostics found')
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
791 export def GetDiagsForBuf(bnr: number = bufnr()): list<dict<any>>
792 if !diagsMap->has_key(bnr) ||
793 diagsMap[bnr].sortedDiagnostics->empty()
797 return diagsMap[bnr].sortedDiagnostics->deepcopy()
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
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)
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)
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()
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)
853 # vim: tabstop=8 shiftwidth=2 softtabstop=2