From: Yegappan Lakshmanan Date: Tue, 11 Jan 2022 05:21:00 +0000 (-0800) Subject: Move all the LSP diagnostic functions to diag.vim and add tests for LSP diagnostics X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=88bb45546655e10b21c401489c2494142e904aec;p=vim-lsp.git Move all the LSP diagnostic functions to diag.vim and add tests for LSP diagnostics --- diff --git a/autoload/diag.vim b/autoload/diag.vim index 763645b..d870422 100644 --- a/autoload/diag.vim +++ b/autoload/diag.vim @@ -1,15 +1,34 @@ vim9script var opt = {} +var util = {} if has('patch-8.2.4019') import './lspoptions.vim' as opt_import + import './util.vim' as util_import + opt.lspOptions = opt_import.lspOptions + util.WarnMsg = util_import.WarnMsg + util.GetLineByteFromPos = util_import.GetLineByteFromPos else import lspOptions from './lspoptions.vim' + import {WarnMsg, + LspUriToFile, + GetLineByteFromPos} from './util.vim' + opt.lspOptions = lspOptions + util.WarnMsg = WarnMsg + util.LspUriToFile = LspUriToFile + util.GetLineByteFromPos = GetLineByteFromPos endif +# Remove the diagnostics stored for buffer 'bnr' +export def DiagRemoveFile(lspserver: dict, bnr: number) + if lspserver.diagsMap->has_key(bnr) + lspserver.diagsMap->remove(bnr) + endif +enddef + def s:lspDiagSevToSignName(severity: number): string var typeMap: list = ['LspDiagError', 'LspDiagWarning', 'LspDiagInfo', 'LspDiagHint'] @@ -21,7 +40,7 @@ enddef # New LSP diagnostic messages received from the server for a file. # Update the signs placed in the buffer for this file -export def LspDiagsUpdated(lspserver: dict, bnr: number) +export def UpdateDiags(lspserver: dict, bnr: number) if !opt.lspOptions.autoHighlightDiags return endif @@ -55,4 +74,162 @@ export def LspDiagsUpdated(lspserver: dict, bnr: number) signs->sign_placelist() enddef +# process a diagnostic notification message from the LSP server +# Notification: textDocument/publishDiagnostics +# Param: PublishDiagnosticsParams +export def DiagNotification(lspserver: dict, uri: string, diags: list>): void + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + # Is this condition possible? + return + endif + + # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here? + var lastlnum: number = bnr->getbufinfo()[0].linecount + var lnum: number + + # store the diagnostic for each line separately + var diag_by_lnum: dict> = {} + for d in diags + lnum = d.range.start.line + 1 + if lnum > lastlnum + # Make sure the line number is a valid buffer line number + lnum = lastlnum + endif + diag_by_lnum[lnum] = d + endfor + + lspserver.diagsMap->extend({['' .. bnr]: diag_by_lnum}) + UpdateDiags(lspserver, bnr) +enddef + +# get the count of error in the current buffer +export def DiagsGetErrorCount(lspserver: dict): dict + var res = {'Error': 0, 'Warn': 0, 'Info': 0, 'Hint': 0} + + var bnr: number = bufnr() + if lspserver.diagsMap->has_key(bnr) + for item in lspserver.diagsMap[bnr]->values() + if item->has_key('severity') + if item.severity == 1 + res.Error = res.Error + 1 + elseif item.severity == 2 + res.Warn = res.Warn + 1 + elseif item.severity == 3 + res.Info = res.Info + 1 + elseif item.severity == 4 + res.Hint = res.Hint + 1 + endif + endif + endfor + endif + + return res +enddef + +# Map the LSP DiagnosticSeverity to a quickfix type character +def s:lspDiagSevToQfType(severity: number): string + var typeMap: list = ['E', 'W', 'I', 'N'] + + if severity > 4 + return '' + endif + + return typeMap[severity - 1] +enddef + +# Display the diagnostic messages from the LSP server for the current buffer +# in a location list +export def ShowAllDiags(lspserver: dict): void + var fname: string = expand('%:p') + if fname == '' + return + endif + var bnr: number = bufnr() + + if !lspserver.diagsMap->has_key(bnr) || lspserver.diagsMap[bnr]->empty() + util.WarnMsg('No diagnostic messages found for ' .. fname) + return + endif + + var qflist: list> = [] + var text: string + + for [lnum, diag] in lspserver.diagsMap[bnr]->items() + text = diag.message->substitute("\n\\+", "\n", 'g') + qflist->add({'filename': fname, + 'lnum': diag.range.start.line + 1, + 'col': util.GetLineByteFromPos(bnr, diag.range.start) + 1, + 'text': text, + 'type': s:lspDiagSevToQfType(diag.severity)}) + endfor + setloclist(0, [], ' ', {'title': 'Language Server Diagnostics', + 'items': qflist}) + :lopen +enddef + +# Show the diagnostic message for the current line +export def ShowCurrentDiag(lspserver: dict) + var bnr: number = bufnr() + var lnum: number = line('.') + var diag: dict = lspserver.getDiagByLine(bnr, lnum) + if diag->empty() + util.WarnMsg('No diagnostic messages found for current line') + else + echo diag.message + endif +enddef + +# Get the diagnostic from the LSP server for a particular line in a file +export def GetDiagByLine(lspserver: dict, bnr: number, lnum: number): dict + if lspserver.diagsMap->has_key(bnr) && + lspserver.diagsMap[bnr]->has_key(lnum) + return lspserver.diagsMap[bnr][lnum] + endif + return {} +enddef + +# sort the diaganostics messages for a buffer by line number +def s:getSortedDiagLines(lspsrv: dict, bnr: number): list + # create a list of line numbers from the diag map keys + var lnums: list = + lspsrv.diagsMap[bnr]->keys()->mapnew((_, v) => v->str2nr()) + return lnums->sort((a, b) => a - b) +enddef + +# jump to the next/previous/first diagnostic message in the current buffer +export def LspDiagsJump(lspserver: dict, which: string): void + var fname: string = expand('%:p') + if fname == '' + return + endif + var bnr: number = bufnr() + + if !lspserver.diagsMap->has_key(bnr) || lspserver.diagsMap[bnr]->empty() + util.WarnMsg('No diagnostic messages found for ' .. fname) + return + endif + + # sort the diagnostics by line number + var sortedDiags: list = s:getSortedDiagLines(lspserver, bnr) + + if which == 'first' + cursor(sortedDiags[0], 1) + return + endif + + # Find the entry just before the current line (binary search) + var curlnum: number = line('.') + for lnum in (which == 'next') ? sortedDiags : sortedDiags->reverse() + if (which == 'next' && lnum > curlnum) + || (which == 'prev' && lnum < curlnum) + cursor(lnum, 1) + return + endif + endfor + + util.WarnMsg('Error: No more diagnostics found') +enddef + # vim: shiftwidth=2 softtabstop=2 diff --git a/autoload/handlers.vim b/autoload/handlers.vim index 02479f6..db6b3e5 100644 --- a/autoload/handlers.vim +++ b/autoload/handlers.vim @@ -19,7 +19,7 @@ if has('patch-8.2.4019') util.TraceLog = util_import.TraceLog util.LspUriToFile = util_import.LspUriToFile util.GetLineByteFromPos = util_import.GetLineByteFromPos - diag.LspDiagsUpdated = diag_import.LspDiagsUpdated + diag.DiagNotification = diag_import.DiagNotification else import lspOptions from './lspoptions.vim' import {WarnMsg, @@ -27,7 +27,7 @@ else TraceLog, LspUriToFile, GetLineByteFromPos} from './util.vim' - import {LspDiagsUpdated} from './diag.vim' + import {DiagNotification} from './diag.vim' opt.lspOptions = lspOptions util.WarnMsg = WarnMsg @@ -35,7 +35,7 @@ else util.TraceLog = TraceLog util.LspUriToFile = LspUriToFile util.GetLineByteFromPos = GetLineByteFromPos - diag.LspDiagsUpdated = LspDiagsUpdated + diag.DiagNotification = DiagNotification endif # process the 'initialize' method reply from the LSP server @@ -583,9 +583,13 @@ def s:set_lines(lines: list, A: list, B: list, var i_n = [B[0], numlines - 1]->min() if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines - util.WarnMsg("set_lines: Invalid range, A = " .. A->string() - .. ", B = " .. B->string() .. ", numlines = " .. numlines - .. ", new lines = " .. new_lines->string()) + #util.WarnMsg("set_lines: Invalid range, A = " .. A->string() + # .. ", B = " .. B->string() .. ", numlines = " .. numlines + # .. ", new lines = " .. new_lines->string()) + var msg = "set_lines: Invalid range, A = " .. A->string() + msg ..= ", B = " .. B->string() .. ", numlines = " .. numlines + msg ..= ", new lines = " .. new_lines->string() + util.WarnMsg(msg) return lines endif @@ -1022,30 +1026,7 @@ enddef # Notification: textDocument/publishDiagnostics # Param: PublishDiagnosticsParams def s:processDiagNotif(lspserver: dict, reply: dict): void - var fname: string = util.LspUriToFile(reply.params.uri) - var bnr: number = fname->bufnr() - if bnr == -1 - # Is this condition possible? - return - endif - - # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here? - var lastlnum: number = bnr->getbufinfo()[0].linecount - var lnum: number - - # store the diagnostic for each line separately - var diag_by_lnum: dict> = {} - for diag in reply.params.diagnostics - lnum = diag.range.start.line + 1 - if lnum > lastlnum - # Make sure the line number is a valid buffer line number - lnum = lastlnum - endif - diag_by_lnum[lnum] = diag - endfor - - lspserver.diagsMap->extend({['' .. bnr]: diag_by_lnum}) - diag.LspDiagsUpdated(lspserver, bnr) + diag.DiagNotification(lspserver, reply.params.uri, reply.params.diagnostics) enddef # process a show notification message from the LSP server diff --git a/autoload/lsp.vim b/autoload/lsp.vim index 33487c4..26793c9 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -22,7 +22,12 @@ if has('patch-8.2.4019') util.ClearTraceLogs = util_import.ClearTraceLogs util.GetLineByteFromPos = util_import.GetLineByteFromPos util.PushCursorToTagStack = util_import.PushCursorToTagStack - diag.LspDiagsUpdated = diag_import.LspDiagsUpdated + diag.UpdateDiags = diag_import.UpdateDiags + diag.DiagsGetErrorCount = diag_import.DiagsGetErrorCount + diag.ShowAllDiags = diag_import.ShowAllDiags + diag.ShowCurrentDiag = diag_import.ShowCurrentDiag + diag.LspDiagsJump = diag_import.LspDiagsJump + diag.DiagRemoveFile = diag_import.DiagRemoveFile else import {lspOptions, LspOptionsSet} from './lspoptions.vim' import NewLspServer from './lspserver.vim' @@ -32,7 +37,12 @@ else ClearTraceLogs, GetLineByteFromPos, PushCursorToTagStack} from './util.vim' - import {LspDiagsUpdated} from './diag.vim' + import {DiagRemoveFile, + UpdateDiags, + DiagsGetErrorCount, + ShowAllDiags, + ShowCurrentDiag, + LspDiagsJump} from './diag.vim' opt.LspOptionsSet = LspOptionsSet opt.lspOptions = lspOptions @@ -43,7 +53,12 @@ else util.ClearTraceLogs = ClearTraceLogs util.GetLineByteFromPos = GetLineByteFromPos util.PushCursorToTagStack = PushCursorToTagStack - diag.LspDiagsUpdated = LspDiagsUpdated + diag.DiagRemoveFile = DiagRemoveFile + diag.UpdateDiags = UpdateDiags + diag.DiagsGetErrorCount = DiagsGetErrorCount + diag.ShowAllDiags = ShowAllDiags + diag.ShowCurrentDiag = ShowCurrentDiag + diag.LspDiagsJump = LspDiagsJump endif # Needs Vim 8.2.2342 and higher @@ -303,7 +318,7 @@ def lsp#leftInsertMode() if lspserver->empty() || !lspserver.running return endif - diag.LspDiagsUpdated(lspserver, bufnr()) + diag.UpdateDiags(lspserver, bufnr()) enddef # A new buffer is opened. If LSP is supported for this buffer, then add it @@ -398,9 +413,7 @@ def lsp#removeFile(bnr: number): void return endif lspserver.textdocDidClose(bnr) - if lspserver.diagsMap->has_key(bnr) - lspserver.diagsMap->remove(bnr) - endif + diag.DiagRemoveFile(lspserver, bnr) bufnrToServer->remove(bnr) enddef @@ -482,22 +495,11 @@ def lsp#setTraceServer(traceVal: string) lspserver.setTrace(traceVal) enddef -# Map the LSP DiagnosticSeverity to a quickfix type character -def s:lspDiagSevToQfType(severity: number): string - var typeMap: list = ['E', 'W', 'I', 'N'] - - if severity > 4 - return '' - endif - - return typeMap[severity - 1] -enddef - # Display the diagnostic messages from the LSP server for the current buffer # in a quickfix list def lsp#showDiagnostics(): void var ftype = &filetype - if ftype == '' + if ftype == '' || @% == '' return endif @@ -511,37 +513,13 @@ def lsp#showDiagnostics(): void return endif - var fname: string = expand('%:p') - if fname == '' - return - endif - var bnr: number = bufnr() - - if !lspserver.diagsMap->has_key(bnr) || lspserver.diagsMap[bnr]->empty() - util.WarnMsg('No diagnostic messages found for ' .. fname) - return - endif - - var qflist: list> = [] - var text: string - - for [lnum, diag] in lspserver.diagsMap[bnr]->items() - text = diag.message->substitute("\n\\+", "\n", 'g') - qflist->add({'filename': fname, - 'lnum': diag.range.start.line + 1, - 'col': util.GetLineByteFromPos(bnr, diag.range.start) + 1, - 'text': text, - 'type': s:lspDiagSevToQfType(diag.severity)}) - endfor - setloclist(0, [], ' ', {'title': 'Language Server Diagnostics', - 'items': qflist}) - :lopen + diag.ShowAllDiags(lspserver) enddef # Show the diagnostic message for the current line def lsp#showCurrentDiag() var ftype = &filetype - if ftype == '' + if ftype == '' || @% == '' return endif @@ -555,14 +533,7 @@ def lsp#showCurrentDiag() return endif - var bnr: number = bufnr() - var lnum: number = line('.') - var diag: dict = lspserver.getDiagByLine(bnr, lnum) - if diag->empty() - util.WarnMsg('No diagnostic messages found for current line') - else - echo diag.message - endif + diag.ShowCurrentDiag(lspserver) enddef # get the count of error in the current buffer @@ -578,38 +549,13 @@ def lsp#errorCount(): dict return res endif - var bnr: number = bufnr() - if lspserver.diagsMap->has_key(bnr) - for item in lspserver.diagsMap[bnr]->values() - if item->has_key('severity') - if item.severity == 1 - res.Error = res.Error + 1 - elseif item.severity == 2 - res.Warn = res.Warn + 1 - elseif item.severity == 3 - res.Info = res.Info + 1 - elseif item.severity == 4 - res.Hint = res.Hint + 1 - endif - endif - endfor - endif - - return res -enddef - -# sort the diaganostics messages for a buffer by line number -def s:getSortedDiagLines(lspsrv: dict, bnr: number): list - # create a list of line numbers from the diag map keys - var lnums: list = - lspsrv.diagsMap[bnr]->keys()->mapnew((_, v) => v->str2nr()) - return lnums->sort((a, b) => a - b) + return diag.DiagsGetErrorCount(lspserver) enddef # jump to the next/previous/first diagnostic message in the current buffer def lsp#jumpToDiag(which: string): void var ftype = &filetype - if ftype == '' + if ftype == '' || @% == '' return endif @@ -623,36 +569,7 @@ def lsp#jumpToDiag(which: string): void return endif - var fname: string = expand('%:p') - if fname == '' - return - endif - var bnr: number = bufnr() - - if !lspserver.diagsMap->has_key(bnr) || lspserver.diagsMap[bnr]->empty() - util.WarnMsg('No diagnostic messages found for ' .. fname) - return - endif - - # sort the diagnostics by line number - var sortedDiags: list = s:getSortedDiagLines(lspserver, bnr) - - if which == 'first' - cursor(sortedDiags[0], 1) - return - endif - - # Find the entry just before the current line (binary search) - var curlnum: number = line('.') - for lnum in (which == 'next') ? sortedDiags : sortedDiags->reverse() - if (which == 'next' && lnum > curlnum) - || (which == 'prev' && lnum < curlnum) - cursor(lnum, 1) - return - endif - endfor - - util.WarnMsg('Error: No more diagnostics found') + diag.LspDiagsJump(lspserver, which) enddef # Insert mode completion handler. Used when 24x7 completion is enabled diff --git a/autoload/lspserver.vim b/autoload/lspserver.vim index c91021a..cef6987 100644 --- a/autoload/lspserver.vim +++ b/autoload/lspserver.vim @@ -5,11 +5,13 @@ vim9script # for the Language Server Protocol (LSP) specificaiton. var handlers = {} +var diag = {} var util = {} if has('patch-8.2.4019') import './handlers.vim' as handlers_import import './util.vim' as util_import + import './diag.vim' as diag_import handlers.ProcessReply = handlers_import.ProcessReply handlers.ProcessNotif = handlers_import.ProcessNotif handlers.ProcessRequest = handlers_import.ProcessRequest @@ -17,19 +19,19 @@ if has('patch-8.2.4019') util.WarnMsg = util_import.WarnMsg util.ErrMsg = util_import.ErrMsg util.TraceLog = util_import.TraceLog - util.LspUriToFile = util_import.LspUriToFile util.LspBufnrToUri = util_import.LspBufnrToUri util.LspFileToUri = util_import.LspFileToUri util.PushCursorToTagStack = util_import.PushCursorToTagStack + diag.GetDiagByLine = diag_import.GetDiagByLine else import {ProcessReply, ProcessNotif, ProcessRequest, ProcessMessages} from './handlers.vim' + import {GetDiagByLine} from './diag.vim' import {WarnMsg, ErrMsg, TraceLog, - LspUriToFile, LspBufnrToUri, LspFileToUri, PushCursorToTagStack} from './util.vim' @@ -40,10 +42,10 @@ else util.WarnMsg = WarnMsg util.ErrMsg = ErrMsg util.TraceLog = TraceLog - util.LspUriToFile = LspUriToFile util.LspBufnrToUri = LspBufnrToUri util.LspFileToUri = LspFileToUri util.PushCursorToTagStack = PushCursorToTagStack + diag.GetDiagByLine = GetDiagByLine endif # LSP server standard output handler @@ -628,8 +630,9 @@ def s:textDocFormat(lspserver: dict, fname: string, rangeFormat: bool, tabSize: tabsz, insertSpaces: &expandtab ? true : false, } - req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}, - options: fmtopts}) + #req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}, + # options: fmtopts}) + req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}, options: fmtopts}) if rangeFormat var r: dict> = { start: {line: start_lnum - 1, character: 0}, @@ -677,15 +680,6 @@ def s:renameSymbol(lspserver: dict, newName: string) lspserver.sendMessage(req) enddef -# Get the diagnostic from the LSP server for a particular line in a file -def s:getDiagByLine(lspserver: dict, bnr: number, lnum: number): dict - if lspserver.diagsMap->has_key(bnr) && - lspserver.diagsMap[bnr]->has_key(lnum) - return lspserver.diagsMap[bnr][lnum] - endif - return {} -enddef - # Request: "textDocument/codeAction" # Param: CodeActionParams def s:codeAction(lspserver: dict, fname_arg: string) @@ -705,13 +699,13 @@ def s:codeAction(lspserver: dict, fname_arg: string) start: {line: line('.') - 1, character: charcol('.') - 1}, end: {line: line('.') - 1, character: charcol('.') - 1}} req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}, range: r}) - var diag: list> = [] + var d: list> = [] var lnum = line('.') - var diagInfo: dict = lspserver.getDiagByLine(bnr, lnum) + var diagInfo: dict = diag.GetDiagByLine(lspserver, bnr, lnum) if !diagInfo->empty() - diag->add(diagInfo) + d->add(diagInfo) endif - req.params->extend({context: {diagnostics: diag}}) + req.params->extend({context: {diagnostics: d}}) lspserver.sendMessage(req) enddef @@ -858,7 +852,7 @@ export def NewLspServer(path: string, args: list): dict processNotif: function(handlers.ProcessNotif, [lspserver]), processRequest: function(handlers.ProcessRequest, [lspserver]), processMessages: function(handlers.ProcessMessages, [lspserver]), - getDiagByLine: function('s:getDiagByLine', [lspserver]), + getDiagByLine: function(diag.GetDiagByLine, [lspserver]), textdocDidOpen: function('s:textdocDidOpen', [lspserver]), textdocDidClose: function('s:textdocDidClose', [lspserver]), textdocDidChange: function('s:textdocDidChange', [lspserver]), diff --git a/autoload/util.vim b/autoload/util.vim index 735febc..bed2f38 100644 --- a/autoload/util.vim +++ b/autoload/util.vim @@ -69,7 +69,7 @@ export def LspUriToFile(uri: string): string return uri_decoded enddef -# Convert a Vim filenmae to an LSP URI (file://) +# Convert a Vim filename to an LSP URI (file://) export def LspFileToUri(fname: string): string var uri: string = fnamemodify(fname, ':p') diff --git a/test/test_lsp.vim b/test/test_lsp.vim index 4036712..b71abb9 100644 --- a/test/test_lsp.vim +++ b/test/test_lsp.vim @@ -138,6 +138,58 @@ def Test_lsp_show_references() :%bw! enddef +def Test_lsp_diags() + :silent! edit Xtest.c + var lines: list =<< trim END + void blueFunc() + { + int count, j: + count = 20; + j <= count; + j = 10; + MyFunc(); + } + END + setline(1, lines) + :sleep 1 + var bnr: number = bufnr() + :redraw! + :LspDiagShow + var qfl: list> = getloclist(0) + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + assert_equal(bnr, qfl[0].bufnr) + assert_equal(3, qfl->len()) + assert_equal([3, 14, 'E'], [qfl[0].lnum, qfl[0].col, qfl[0].type]) + assert_equal([5, 2, 'W'], [qfl[1].lnum, qfl[1].col, qfl[1].type]) + assert_equal([7, 2, 'W'], [qfl[2].lnum, qfl[2].col, qfl[2].type]) + close + normal gg + var output = execute('LspDiagCurrent')->split("\n") + assert_equal('No diagnostic messages found for current line', output[0]) + :LspDiagFirst + assert_equal([3, 1], [line('.'), col('.')]) + output = execute('LspDiagCurrent')->split("\n") + assert_equal("Expected ';' at end of declaration (fix available)", output[0]) + :LspDiagNext + assert_equal([5, 1], [line('.'), col('.')]) + :LspDiagNext + assert_equal([7, 1], [line('.'), col('.')]) + output = execute('LspDiagNext')->split("\n") + assert_equal('Error: No more diagnostics found', output[0]) + :LspDiagPrev + :LspDiagPrev + :LspDiagPrev + output = execute('LspDiagPrev')->split("\n") + assert_equal('Error: No more diagnostics found', output[0]) + :%d + setline(1, ['void blueFunc()', '{', '}']) + sleep 1 + output = execute('LspDiagShow')->split("\n") + assert_match('No diagnostic messages found for', output[0]) + + :%bw! +enddef + def LspRunTests() # Edit a dummy C file to start the LSP server :edit Xtest.c @@ -150,11 +202,15 @@ def LspRunTests() ->map("v:val->substitute('^def \\d\\+_', '', '')") for f in fns v:errors = [] + v:errmsg = '' try exe f catch - echomsg "Error: Test " .. f .. " failed with exception " .. v:exception + call add(v:errors, "Error: Test " .. f .. " failed with exception " .. v:exception) endtry + if v:errmsg != '' + call add(v:errors, "Error: Test " .. f .. " generated error " .. v:errmsg) + endif if v:errors->len() != 0 new Lsp-Test-Results setline(1, ["Error: Test " .. f .. " failed"]->extend(v:errors))