From 9920f9973bb5869438ac2fd9ef4e3c862e60d578 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Wed, 6 Jan 2021 07:07:24 -0800 Subject: [PATCH] Add signs for each of the LSP diagnostic line --- autoload/buf.vim | 36 +++++++++++++++++++++++ autoload/handlers.vim | 51 ++++++++++++++++++++++++++++---- autoload/lsp.vim | 66 ++++++++++++++++++++++++++++++++---------- autoload/lspserver.vim | 17 +++++++++-- 4 files changed, 146 insertions(+), 24 deletions(-) create mode 100644 autoload/buf.vim diff --git a/autoload/buf.vim b/autoload/buf.vim new file mode 100644 index 0000000..5410310 --- /dev/null +++ b/autoload/buf.vim @@ -0,0 +1,36 @@ +vim9script + +def s:lspDiagSevToSignName(severity: number): string + var typeMap: list = ['LspDiagError', 'LspDiagWarning', + 'LspDiagInfo', 'LspDiagHint'] + if severity > 4 + return 'LspDiagHint' + endif + return typeMap[severity - 1] +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) + if bnr == -1 || !lspserver.diagsMap->has_key(bnr) + return + endif + + # Remove all the existing diagnostic signs + sign_unplace('LSPDiag') + + if lspserver.diagsMap[bnr]->empty() + return + endif + + var signs: list> = [] + for [lnum, diag] in items(lspserver.diagsMap[bnr]) + signs->add({id: 0, buffer: str2nr(bnr), group: 'LSPDiag', + lnum: str2nr(lnum), + name: s:lspDiagSevToSignName(diag.severity)}) + endfor + + signs->sign_placelist() +enddef + +# vim: shiftwidth=2 softtabstop=2 diff --git a/autoload/handlers.vim b/autoload/handlers.vim index 4cfe3ab..d462959 100644 --- a/autoload/handlers.vim +++ b/autoload/handlers.vim @@ -5,6 +5,7 @@ vim9script # for the Language Server Protocol (LSP) specificaiton. import {WarnMsg, ErrMsg, TraceLog, LspUriToFile} from './util.vim' +import {LspDiagsUpdated} from './buf.vim' # process the 'initialize' method reply from the LSP server # Result: InitializeResult @@ -760,6 +761,7 @@ def s:processFoldingRangeReply(lspserver: dict, req: dict, reply: dict enddef # process the 'workspace/executeCommand' reply from the LSP server +# Result: any | null def s:processWorkspaceExecuteReply(lspserver: dict, req: dict, reply: dict) if reply.result->empty() return @@ -864,20 +866,37 @@ export def ProcessReply(lspserver: dict, req: dict, reply: dict): enddef # process a diagnostic notification message from the LSP server +# Notification: textDocument/publishDiagnostics # Param: PublishDiagnosticsParams def s:processDiagNotif(lspserver: dict, reply: dict): void var fname: string = LspUriToFile(reply.params.uri) + var bnr: number = bufnr(fname) + 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 - diag_by_lnum[diag.range.start.line + 1] = diag + 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({[fname]: diag_by_lnum}) + lspserver.diagsMap->extend({['' .. bnr]: diag_by_lnum}) + LspDiagsUpdated(lspserver, bnr) enddef # process a show notification message from the LSP server +# Notification: window/showMessage # Param: ShowMessageParams def s:processShowMsgNotif(lspserver: dict, reply: dict) var msgType: list = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: '] @@ -897,6 +916,7 @@ def s:processShowMsgNotif(lspserver: dict, reply: dict) enddef # process a log notification message from the LSP server +# Notification: window/logMessage # Param: LogMessageParams def s:processLogMsgNotif(lspserver: dict, reply: dict) var msgType: list = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: '] @@ -908,13 +928,20 @@ def s:processLogMsgNotif(lspserver: dict, reply: dict) TraceLog(false, '[' .. mtype .. ']: ' .. reply.params.message) enddef +# process unsupported notification messages +def s:processUnsupportedNotif(lspserver: dict, reply: dict) + ErrMsg('Error: Unsupported notification message received from the LSP server (' .. lspserver.path .. '), message = ' .. string(reply)) +enddef + # process notification messages from the LSP server export def ProcessNotif(lspserver: dict, reply: dict): void var lsp_notif_handlers: dict = { - 'textDocument/publishDiagnostics': function('s:processDiagNotif'), 'window/showMessage': function('s:processShowMsgNotif'), - 'window/logMessage': function('s:processLogMsgNotif') + 'window/logMessage': function('s:processLogMsgNotif'), + 'textDocument/publishDiagnostics': function('s:processDiagNotif'), + '$/progress': function('s:processUnsupportedNotif'), + 'telemetry/event': function('s:processUnsupportedNotif') } if lsp_notif_handlers->has_key(reply.method) @@ -925,6 +952,7 @@ export def ProcessNotif(lspserver: dict, reply: dict): void enddef # process the workspace/applyEdit LSP server request +# Request: "workspace/applyEdit" # Param: ApplyWorkspaceEditParams def s:processApplyEditReq(lspserver: dict, request: dict) # interface ApplyWorkspaceEditParams @@ -940,11 +968,22 @@ def s:processApplyEditReq(lspserver: dict, request: dict) lspserver.sendResponse(request, {applied: v:true}, v:null) enddef +def s:processUnsupportedReq(lspserver: dict, request: dict) + ErrMsg('Error: Unsupported request message received from the LSP server (' .. lspserver.path .. '), message = ' .. string(request)) +enddef + # process a request message from the server export def ProcessRequest(lspserver: dict, request: dict) var lspRequestHandlers: dict = { - 'workspace/applyEdit': function('s:processApplyEditReq') + 'workspace/applyEdit': function('s:processApplyEditReq'), + 'window/workDoneProgress/create': function('s:processUnsupportedReq'), + 'client/registerCapability': function('s:processUnsupportedReq'), + 'client/unregisterCapability': function('s:processUnsupportedReq'), + 'workspace/workspaceFolders': function('s:processUnsupportedReq'), + 'workspace/configuration': function('s:processUnsupportedReq'), + 'workspace/codeLens/refresh': function('s:processUnsupportedReq'), + 'workspace/semanticTokens/refresh': function('s:processUnsupportedReq') } if lspRequestHandlers->has_key(request.method) @@ -998,7 +1037,7 @@ export def ProcessMessages(lspserver: dict): void else # request failed var emsg: string = msg.error.message - emsg ..= ', code = ' .. msg.code + emsg ..= ', code = ' .. msg.error.code if msg.error->has_key('data') emsg = emsg .. ', data = ' .. string(msg.error.data) endif diff --git a/autoload/lsp.vim b/autoload/lsp.vim index f983c2f..b33d5a5 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -19,12 +19,25 @@ var ftypeServerMap: dict> = {} # Buffer number to LSP server map var bufnrToServer: dict> = {} -# List of diagnostics for each opened file -#var diagsMap: dict> = {} - -prop_type_add('LspTextRef', {'highlight': 'Search'}) -prop_type_add('LspReadRef', {'highlight': 'DiffChange'}) -prop_type_add('LspWriteRef', {'highlight': 'DiffDelete'}) +var lspInitializedOnce = false + +def s:lspInitOnce() + # Signs used for LSP diagnostics + sign_define([{name: 'LspDiagError', text: 'E ', texthl: 'ErrorMsg', + linehl: 'MatchParen'}, + {name: 'LspDiagWarning', text: 'W ', texthl: 'Search', + linehl: 'MatchParen'}, + {name: 'LspDiagInfo', text: 'I ', texthl: 'Pmenu', + linehl: 'MatchParen'}, + {name: 'LspDiagHint', text: 'H ', texthl: 'Question', + linehl: 'MatchParen'}]) + + prop_type_add('LspTextRef', {'highlight': 'Search'}) + prop_type_add('LspReadRef', {'highlight': 'DiffChange'}) + prop_type_add('LspWriteRef', {'highlight': 'DiffDelete'}) + set ballooneval balloonevalterm + lspInitializedOnce = true +enddef # Return the LSP server for the a specific filetype. Returns a null dict if # the server is not found. @@ -187,6 +200,25 @@ def s:lspSavedFile() lspserver.didSaveFile(bnr) enddef +# Return the diagnostic text from the LSP server for the current mouse line to +# display in a balloon +var lspDiagPopupID: number = 0 +var lspDiagPopupInfo: dict = {} +def g:LspDiagExpr(): string + var lspserver: dict = bufnrToServer->get(v:beval_bufnr, {}) + if lspserver->empty() || !lspserver.running + return '' + endif + + var diagInfo: dict = lspserver.getDiagByLine(v:beval_bufnr, v:beval_lnum) + if diagInfo->empty() + # No diagnostic for the current cursor location + return '' + endif + + return diagInfo.message +enddef + # A new buffer is opened. If LSP is supported for this buffer, then add it def lsp#addFile(bnr: number): void if bufnrToServer->has_key(bnr) @@ -203,6 +235,9 @@ def lsp#addFile(bnr: number): void return endif if !lspserver.running + if !lspInitializedOnce + s:lspInitOnce() + endif lspserver.startServer() endif lspserver.textdocDidOpen(bnr, ftype) @@ -217,6 +252,7 @@ def lsp#addFile(bnr: number): void setbufvar(bnr, '&completefunc', 'lsp#completeFunc') setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect') setbufvar(bnr, '&completepopup', 'border:off') + setbufvar(bnr, '&balloonexpr', 'LspDiagExpr()') # map characters that trigger signature help if lspserver.caps->has_key('signatureHelpProvider') @@ -255,8 +291,8 @@ def lsp#removeFile(bnr: number): void return endif lspserver.textdocDidClose(bnr) - if lspserver.diagsMap->has_key(fname) - lspserver.diagsMap->remove(fname) + if lspserver.diagsMap->has_key(bnr) + lspserver.diagsMap->remove(bnr) endif remove(bufnrToServer, bnr) enddef @@ -329,8 +365,8 @@ def lsp#setTraceServer(traceVal: string) lspserver.setTrace(traceVal) enddef -# Map the LSP DiagnosticSeverity to a type character -def LspDiagSevToType(severity: number): string +# 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 @@ -361,8 +397,9 @@ def lsp#showDiagnostics(): void if fname == '' return endif + var bnr: number = bufnr() - if !lspserver.diagsMap->has_key(fname) || lspserver.diagsMap[fname]->empty() + if !lspserver.diagsMap->has_key(bnr) || lspserver.diagsMap[bnr]->empty() WarnMsg('No diagnostic messages found for ' .. fname) return endif @@ -370,13 +407,13 @@ def lsp#showDiagnostics(): void var qflist: list> = [] var text: string - for [lnum, diag] in items(lspserver.diagsMap[fname]) + for [lnum, diag] in items(lspserver.diagsMap[bnr]) text = diag.message->substitute("\n\\+", "\n", 'g') qflist->add({'filename': fname, 'lnum': diag.range.start.line + 1, 'col': diag.range.start.character + 1, 'text': text, - 'type': LspDiagSevToType(diag.severity)}) + 'type': s:lspDiagSevToQfType(diag.severity)}) endfor setqflist([], ' ', {'title': 'Language Server Diagnostics', 'items': qflist}) :copen @@ -584,7 +621,7 @@ def s:addSymbolText(symbolTypeTable: dict>>, lnumMap->add({name: s.name, lnum: s.range.start.line + 1, col: s.range.start.character + 1}) s.outlineLine = lnumMap->len() - if !s.children->empty() + if s->has_key('children') && !s.children->empty() s:addSymbolText(s.children, prefix, text, lnumMap, true) endif endfor @@ -976,7 +1013,6 @@ def s:jumpToWorkspaceSymbol(popupID: number, result: number): void endif var symTbl: list> = popupID->getwinvar('LspSymbolTable') - echomsg symTbl try # if the selected file is already present in a window, then jump to it var fname: string = symTbl[result - 1].file diff --git a/autoload/lspserver.vim b/autoload/lspserver.vim index 6e29885..90a0009 100644 --- a/autoload/lspserver.vim +++ b/autoload/lspserver.vim @@ -626,6 +626,15 @@ 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) @@ -640,6 +649,7 @@ def s:codeAction(lspserver: dict, fname_arg: string) # interface CodeActionParams var fname: string = fnamemodify(fname_arg, ':p') + var bnr: number = bufnr(fname_arg) req.params->extend({textDocument: {uri: LspFileToUri(fname)}}) var r: dict> = { start: {line: line('.') - 1, character: col('.') - 1}, @@ -647,9 +657,9 @@ def s:codeAction(lspserver: dict, fname_arg: string) req.params->extend({range: r}) var diag: list> = [] var lnum = line('.') - if lspserver.diagsMap->has_key(fname) && - lspserver.diagsMap[fname]->has_key(lnum) - diag->add(lspserver.diagsMap[fname][lnum]) + var diagInfo: dict = lspserver.getDiagByLine(bnr, lnum) + if !diagInfo->empty() + diag->add(diagInfo) endif req.params->extend({context: {diagnostics: diag}}) @@ -797,6 +807,7 @@ export def NewLspServer(path: string, args: list): dict processNotif: function('ProcessNotif', [lspserver]), processRequest: function('ProcessRequest', [lspserver]), processMessages: function('ProcessMessages', [lspserver]), + getDiagByLine: function('s:getDiagByLine', [lspserver]), textdocDidOpen: function('s:textdocDidOpen', [lspserver]), textdocDidClose: function('s:textdocDidClose', [lspserver]), textdocDidChange: function('s:textdocDidChange', [lspserver]), -- 2.48.1