--- /dev/null
+vim9script
+
+def s:lspDiagSevToSignName(severity: number): string
+ var typeMap: list<string> = ['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<any>, 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<dict<any>> = []
+ 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
# 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
enddef
# process the 'workspace/executeCommand' reply from the LSP server
+# Result: any | null
def s:processWorkspaceExecuteReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
if reply.result->empty()
return
enddef
# process a diagnostic notification message from the LSP server
+# Notification: textDocument/publishDiagnostics
# Param: PublishDiagnosticsParams
def s:processDiagNotif(lspserver: dict<any>, reply: dict<any>): 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<dict<any>> = {}
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<any>, reply: dict<any>)
var msgType: list<string> = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: ']
enddef
# process a log notification message from the LSP server
+# Notification: window/logMessage
# Param: LogMessageParams
def s:processLogMsgNotif(lspserver: dict<any>, reply: dict<any>)
var msgType: list<string> = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: ']
TraceLog(false, '[' .. mtype .. ']: ' .. reply.params.message)
enddef
+# process unsupported notification messages
+def s:processUnsupportedNotif(lspserver: dict<any>, reply: dict<any>)
+ 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<any>, reply: dict<any>): void
var lsp_notif_handlers: dict<func> =
{
- '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)
enddef
# process the workspace/applyEdit LSP server request
+# Request: "workspace/applyEdit"
# Param: ApplyWorkspaceEditParams
def s:processApplyEditReq(lspserver: dict<any>, request: dict<any>)
# interface ApplyWorkspaceEditParams
lspserver.sendResponse(request, {applied: v:true}, v:null)
enddef
+def s:processUnsupportedReq(lspserver: dict<any>, request: dict<any>)
+ 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<any>, request: dict<any>)
var lspRequestHandlers: dict<func> =
{
- '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)
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
# Buffer number to LSP server map
var bufnrToServer: dict<dict<any>> = {}
-# List of diagnostics for each opened file
-#var diagsMap: dict<dict<any>> = {}
-
-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.
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<any> = {}
+def g:LspDiagExpr(): string
+ var lspserver: dict<any> = bufnrToServer->get(v:beval_bufnr, {})
+ if lspserver->empty() || !lspserver.running
+ return ''
+ endif
+
+ var diagInfo: dict<any> = 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)
return
endif
if !lspserver.running
+ if !lspInitializedOnce
+ s:lspInitOnce()
+ endif
lspserver.startServer()
endif
lspserver.textdocDidOpen(bnr, ftype)
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')
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
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<string> = ['E', 'W', 'I', 'N']
if severity > 4
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
var qflist: list<dict<any>> = []
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
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
endif
var symTbl: list<dict<any>> = 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
lspserver.sendMessage(req)
enddef
+# Get the diagnostic from the LSP server for a particular line in a file
+def s:getDiagByLine(lspserver: dict<any>, bnr: number, lnum: number): dict<any>
+ 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<any>, 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<dict<number>> = {
start: {line: line('.') - 1, character: col('.') - 1},
req.params->extend({range: r})
var diag: list<dict<any>> = []
var lnum = line('.')
- if lspserver.diagsMap->has_key(fname) &&
- lspserver.diagsMap[fname]->has_key(lnum)
- diag->add(lspserver.diagsMap[fname][lnum])
+ var diagInfo: dict<any> = lspserver.getDiagByLine(bnr, lnum)
+ if !diagInfo->empty()
+ diag->add(diagInfo)
endif
req.params->extend({context: {diagnostics: diag}})
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]),