From 44d7a7648c1b8fa8ebd2143de3719f6045bef10f Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Wed, 30 Dec 2020 19:27:04 -0800 Subject: [PATCH] Refactor the code --- autoload/handlers.vim | 904 ++++++++++++++++++++ autoload/lsp.vim | 1830 ++-------------------------------------- autoload/lspserver.vim | 715 ++++++++++++++++ autoload/util.vim | 91 ++ 4 files changed, 1773 insertions(+), 1767 deletions(-) create mode 100644 autoload/handlers.vim create mode 100644 autoload/lspserver.vim create mode 100644 autoload/util.vim diff --git a/autoload/handlers.vim b/autoload/handlers.vim new file mode 100644 index 0000000..1276d94 --- /dev/null +++ b/autoload/handlers.vim @@ -0,0 +1,904 @@ +vim9script + +import {WarnMsg, ErrMsg, LspUriToFile} from './util.vim' + +# process the 'initialize' method reply from the LSP server +def s:processInitializeReply(lspserver: dict, req: dict, reply: dict): void + if reply.result->len() <= 0 + return + endif + + # interface 'InitializeResult' + var caps: dict = reply.result.capabilities + lspserver.caps = caps + + # TODO: Check all the buffers with filetype corresponding to this LSP server + # and then setup the below mapping for those buffers. + + # map characters that trigger signature help + if caps->has_key('signatureHelpProvider') + var triggers = caps.signatureHelpProvider.triggerCharacters + for ch in triggers + exe 'inoremap ' .. ch .. ' ' .. ch .. "=lsp#showSignature()" + endfor + endif + + # map characters that trigger insert mode completion + if caps->has_key('completionProvider') + var triggers = caps.completionProvider.triggerCharacters + for ch in triggers + exe 'inoremap ' .. ch .. ' ' .. ch .. "" + endfor + endif + + # send a "initialized" notification to server + lspserver.sendInitializedNotif() +enddef + +# process the 'textDocument/definition' / 'textDocument/declaration' / +# 'textDocument/typeDefinition' and 'textDocument/implementation' replies from +# the LSP server +def s:processDefDeclReply(lspserver: dict, req: dict, reply: dict): void + if reply.result->empty() + WarnMsg("Error: definition is not found") + # pop the tag stack + var tagstack: dict = gettagstack() + if tagstack.length > 0 + settagstack(winnr(), {'curidx': tagstack.length}, 't') + endif + return + endif + + var result: dict = reply.result[0] + var file = LspUriToFile(result.uri) + var wid = bufwinid(file) + if wid != -1 + win_gotoid(wid) + else + exe 'split ' .. file + endif + # Set the previous cursor location mark + setpos("'`", getcurpos()) + cursor(result.range.start.line + 1, result.range.start.character + 1) + redraw! +enddef + +# process the 'textDocument/signatureHelp' reply from the LSP server +def s:processSignaturehelpReply(lspserver: dict, req: dict, reply: dict): void + var result: dict = reply.result + if result.signatures->len() <= 0 + WarnMsg('No signature help available') + return + endif + + var sig: dict = result.signatures[result.activeSignature] + var text = sig.label + var hllen = 0 + var startcol = 0 + if sig->has_key('parameters') + var params_len = sig.parameters->len() + if params_len > 0 && result.activeParameter < params_len + var label = sig.parameters[result.activeParameter].label + hllen = label->len() + startcol = text->stridx(label) + endif + endif + var popupID = popup_atcursor(text, {}) + prop_type_add('signature', {'bufnr': popupID->winbufnr(), 'highlight': 'Title'}) + if hllen > 0 + prop_add(1, startcol + 1, {'bufnr': popupID->winbufnr(), 'length': hllen, 'type': 'signature'}) + endif +enddef + +# Map LSP complete item kind to a character +def LspCompleteItemKindChar(kind: number): string + var kindMap: list = ['', + 't', # Text + 'm', # Method + 'f', # Function + 'C', # Constructor + 'F', # Field + 'v', # Variable + 'c', # Class + 'i', # Interface + 'M', # Module + 'p', # Property + 'u', # Unit + 'V', # Value + 'e', # Enum + 'k', # Keyword + 'S', # Snippet + 'C', # Color + 'f', # File + 'r', # Reference + 'F', # Folder + 'E', # EnumMember + 'd', # Contant + 's', # Struct + 'E', # Event + 'o', # Operator + 'T' # TypeParameter + ] + if kind > 25 + return '' + endif + return kindMap[kind] +enddef + +# process the 'textDocument/completion' reply from the LSP server +def s:processCompletionReply(lspserver: dict, req: dict, reply: dict): void + + var items: list> + if type(reply.result) == v:t_list + items = reply.result + else + items = reply.result.items + endif + + for item in items + var d: dict = {} + if item->has_key('insertText') + d.word = item.insertText + elseif item->has_key('textEdit') + d.word = item.textEdit.newText + else + d.word = item.label + endif + if item->has_key('kind') + # namespace CompletionItemKind + # map LSP kind to complete-item-kind + d.kind = LspCompleteItemKindChar(item.kind) + endif + if item->has_key('detail') + d.menu = item.detail + endif + if item->has_key('documentation') + d.info = item.documentation + endif + lspserver.completeItems->add(d) + endfor + + lspserver.completePending = v:false +enddef + +# process the 'textDocument/hover' reply from the LSP server +def s:processHoverReply(lspserver: dict, req: dict, reply: dict): void + if type(reply.result) == v:t_none + return + endif + + var hoverText: list + + if type(reply.result.contents) == v:t_dict + if reply.result.contents->has_key('kind') + # MarkupContent + if reply.result.contents.kind == 'plaintext' + hoverText = reply.result.contents.value->split("\n") + elseif reply.result.contents.kind == 'markdown' + hoverText = reply.result.contents.value->split("\n") + else + ErrMsg('Error: Unsupported hover contents type (' .. reply.result.contents.kind .. ')') + return + endif + elseif reply.result.contents->has_key('value') + # MarkedString + hoverText = reply.result.contents.value + else + ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')') + return + endif + elseif type(reply.result.contents) == v:t_list + # interface MarkedString[] + for e in reply.result.contents + if type(e) == v:t_string + hoverText->extend(e->split("\n")) + else + hoverText->extend(e.value->split("\n")) + endif + endfor + elseif type(reply.result.contents) == v:t_string + if reply.result.contents->empty() + return + endif + hoverText->add(reply.result.contents) + else + ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')') + return + endif + hoverText->popup_atcursor({'moved': 'word'}) +enddef + +# process the 'textDocument/references' reply from the LSP server +def s:processReferencesReply(lspserver: dict, req: dict, reply: dict): void + if type(reply.result) == v:t_none || reply.result->empty() + WarnMsg('Error: No references found') + return + endif + + # create a quickfix list with the location of the references + var locations: list> = reply.result + var qflist: list> = [] + for loc in locations + var fname: string = LspUriToFile(loc.uri) + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + bnr->bufload() + endif + var text: string = bnr->getbufline(loc.range.start.line + 1)[0] + ->trim("\t ", 1) + qflist->add({'filename': fname, + 'lnum': loc.range.start.line + 1, + 'col': loc.range.start.character + 1, + 'text': text}) + endfor + setqflist([], ' ', {'title': 'Language Server', 'items': qflist}) + var save_winid = win_getid() + copen + win_gotoid(save_winid) +enddef + +# process the 'textDocument/documentHighlight' reply from the LSP server +def s:processDocHighlightReply(lspserver: dict, req: dict, reply: dict): void + if reply.result->empty() + return + endif + + var fname: string = LspUriToFile(req.params.textDocument.uri) + var bnr = bufnr(fname) + + for docHL in reply.result + var kind: number = docHL->get('kind', 1) + var propName: string + if kind == 2 + # Read-access + propName = 'LspReadRef' + elseif kind == 3 + # Write-access + propName = 'LspWriteRef' + else + # textual reference + propName = 'LspTextRef' + endif + prop_add(docHL.range.start.line + 1, docHL.range.start.character + 1, + {'end_lnum': docHL.range.end.line + 1, + 'end_col': docHL.range.end.character + 1, + 'bufnr': bnr, + 'type': propName}) + endfor +enddef + +# map the LSP symbol kind number to string +def LspSymbolKindToName(symkind: number): string + var symbolMap: list = ['', 'File', 'Module', 'Namespace', 'Package', + 'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum', + 'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number', + 'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct', + 'Event', 'Operator', 'TypeParameter'] + if symkind > 26 + return '' + endif + return symbolMap[symkind] +enddef + +# jump to a symbol selected in the symbols window +def handlers#jumpToSymbol() + var lnum: number = line('.') - 1 + if w:lsp_info.data[lnum]->empty() + return + endif + + var slnum: number = w:lsp_info.data[lnum].lnum + var scol: number = w:lsp_info.data[lnum].col + var fname: string = w:lsp_info.filename + + # If the file is already opened in a window, jump to it. Otherwise open it + # in another window + var wid: number = fname->bufwinid() + if wid == -1 + # Find a window showing a normal buffer and use it + for w in getwininfo() + if w.winid->getwinvar('&buftype') == '' + wid = w.winid + wid->win_gotoid() + break + endif + endfor + if wid == -1 + var symWinid: number = win_getid() + :rightbelow vnew + # retain the fixed symbol window width + win_execute(symWinid, 'vertical resize 20') + endif + + exe 'edit ' .. fname + else + wid->win_gotoid() + endif + [slnum, scol]->cursor() +enddef + +# display the list of document symbols from the LSP server in a window as a +# tree +def s:showSymbols(symTable: list>) + var symbols: dict>> + var symbolType: string + var fname: string + + for symbol in symTable + if symbol->has_key('location') + fname = LspUriToFile(symbol.location.uri) + symbolType = LspSymbolKindToName(symbol.kind) + if !symbols->has_key(symbolType) + symbols[symbolType] = [] + endif + var name: string = symbol.name + if symbol->has_key('containerName') + if symbol.containerName != '' + name ..= ' [' .. symbol.containerName .. ']' + endif + endif + symbols[symbolType]->add({'name': name, + 'lnum': symbol.location.range.start.line + 1, + 'col': symbol.location.range.start.character + 1}) + endif + endfor + + var wid: number = bufwinid('LSP-Symbols') + if wid == -1 + :20vnew LSP-Symbols + else + win_gotoid(wid) + endif + + :setlocal modifiable + :setlocal noreadonly + :silent! :%d _ + :setlocal buftype=nofile + :setlocal bufhidden=delete + :setlocal noswapfile nobuflisted + :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth + setline(1, ['# Language Server Symbols', '# ' .. fname]) + # First two lines in the buffer display comment information + var lnumMap: list> = [{}, {}] + var text: list = [] + for [symType, syms] in items(symbols) + text->extend(['', symType]) + lnumMap->extend([{}, {}]) + for s in syms + text->add(' ' .. s.name) + lnumMap->add({'lnum': s.lnum, 'col': s.col}) + endfor + endfor + append(line('$'), text) + w:lsp_info = {'filename': fname, 'data': lnumMap} + :nnoremap q :quit + :nnoremap :call handlers#jumpToSymbol() + :setlocal nomodifiable +enddef + +# process the 'textDocument/documentSymbol' reply from the LSP server +# Open a symbols window and display the symbols as a tree +def s:processDocSymbolReply(lspserver: dict, req: dict, reply: dict): void + if reply.result->empty() + WarnMsg('No symbols are found') + return + endif + + s:showSymbols(reply.result) +enddef + +# Returns the byte number of the specified line/col position. Returns a +# zero-indexed column. 'pos' is LSP "interface position". +def s:get_line_byte_from_position(bnr: number, pos: dict): number + # LSP's line and characters are 0-indexed + # Vim's line and columns are 1-indexed + var col: number = pos.character + # When on the first character, we can ignore the difference between byte and + # character + if col > 0 + if !bnr->bufloaded() + bnr->bufload() + endif + + var ltext: list = bnr->getbufline(pos.line + 1) + if !ltext->empty() + var bidx = ltext[0]->byteidx(col) + if bidx != -1 + return bidx + endif + endif + endif + + return col +enddef + +# sort the list of edit operations in the descending order of line and column +# numbers. +# 'a': {'A': [lnum, col], 'B': [lnum, col]} +# 'b': {'A': [lnum, col], 'B': [lnum, col]} +def s:edit_sort_func(a: dict, b: dict): number + # line number + if a.A[0] != b.A[0] + return b.A[0] - a.A[0] + endif + # column number + if a.A[1] != b.A[1] + return b.A[1] - a.A[1] + endif + + return 0 +enddef + +# Replaces text in a range with new text. +# +# CAUTION: Changes in-place! +# +# 'lines': Original list of strings +# 'A': Start position; [line, col] +# 'B': End position [line, col] +# 'new_lines' A list of strings to replace the original +# +# returns the modified 'lines' +def s:set_lines(lines: list, A: list, B: list, + new_lines: list): list + var i_0: number = A[0] + + # If it extends past the end, truncate it to the end. This is because the + # way the LSP describes the range including the last newline is by + # specifying a line number after what we would call the last line. + var numlines: number = lines->len() + var i_n = [B[0], numlines - 1]->min() + + if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines + WarnMsg("set_lines: Invalid range, A = " .. string(A) + .. ", B = " .. string(B) .. ", numlines = " .. numlines + .. ", new lines = " .. string(new_lines)) + return lines + endif + + # save the prefix and suffix text before doing the replacements + var prefix: string = '' + var suffix: string = lines[i_n][B[1] :] + if A[1] > 0 + prefix = lines[i_0][0 : A[1] - 1] + endif + + var new_lines_len: number = new_lines->len() + + #echomsg 'i_0 = ' .. i_0 .. ', i_n = ' .. i_n .. ', new_lines = ' .. string(new_lines) + var n: number = i_n - i_0 + 1 + if n != new_lines_len + if n > new_lines_len + # remove the deleted lines + lines->remove(i_0, i_0 + n - new_lines_len - 1) + else + # add empty lines for newly the added lines (will be replaced with the + # actual lines below) + lines->extend(repeat([''], new_lines_len - n), i_0) + endif + endif + #echomsg "lines(1) = " .. string(lines) + + # replace the previous lines with the new lines + for i in range(new_lines_len) + lines[i_0 + i] = new_lines[i] + endfor + #echomsg "lines(2) = " .. string(lines) + + # append the suffix (if any) to the last line + if suffix != '' + var i = i_0 + new_lines_len - 1 + lines[i] = lines[i] .. suffix + endif + #echomsg "lines(3) = " .. string(lines) + + # prepend the prefix (if any) to the first line + if prefix != '' + lines[i_0] = prefix .. lines[i_0] + endif + #echomsg "lines(4) = " .. string(lines) + + return lines +enddef + +# Apply set of text edits to the specified buffer +# The text edit logic is ported from the Neovim lua implementation +def s:applyTextEdits(bnr: number, text_edits: list>): void + if text_edits->empty() + return + endif + + # if the buffer is not loaded, load it and make it a listed buffer + if !bnr->bufloaded() + bnr->bufload() + endif + bnr->setbufvar('&buflisted', v:true) + + var start_line: number = 4294967295 # 2 ^ 32 + var finish_line: number = -1 + var updated_edits: list> = [] + var start_row: number + var start_col: number + var end_row: number + var end_col: number + + # create a list of buffer positions where the edits have to be applied. + for e in text_edits + # Adjust the start and end columns for multibyte characters + start_row = e.range.start.line + start_col = s:get_line_byte_from_position(bnr, e.range.start) + end_row = e.range.end.line + end_col = s:get_line_byte_from_position(bnr, e.range.end) + start_line = [e.range.start.line, start_line]->min() + finish_line = [e.range.end.line, finish_line]->max() + + updated_edits->add({'A': [start_row, start_col], + 'B': [end_row, end_col], + 'lines': e.newText->split("\n", v:true)}) + endfor + + # Reverse sort the edit operations by descending line and column numbers so + # that they can be applied without interfering with each other. + updated_edits->sort('s:edit_sort_func') + + var lines: list = bnr->getbufline(start_line + 1, finish_line + 1) + var fix_eol: number = bnr->getbufvar('&fixeol') + var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1 + if set_eol && lines[-1]->len() != 0 + lines->add('') + endif + + #echomsg 'lines(1) = ' .. string(lines) + #echomsg updated_edits + + for e in updated_edits + var A: list = [e.A[0] - start_line, e.A[1]] + var B: list = [e.B[0] - start_line, e.B[1]] + lines = s:set_lines(lines, A, B, e.lines) + endfor + + #echomsg 'lines(2) = ' .. string(lines) + + # If the last line is empty and we need to set EOL, then remove it. + if set_eol && lines[-1]->len() == 0 + lines->remove(-1) + endif + + #echomsg 'applyTextEdits: start_line = ' .. start_line .. ', finish_line = ' .. finish_line + #echomsg 'lines = ' .. string(lines) + + # Delete all the lines that need to be modified + bnr->deletebufline(start_line + 1, finish_line + 1) + + # if the buffer is empty, appending lines before the first line adds an + # extra empty line at the end. Delete the empty line after appending the + # lines. + var dellastline: bool = v:false + if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 && + bnr->getbufline(1)[0] == '' + dellastline = v:true + endif + + # Append the updated lines + appendbufline(bnr, start_line, lines) + + if dellastline + bnr->deletebufline(bnr->getbufinfo()[0].linecount) + endif +enddef + +# interface TextDocumentEdit +def s:applyTextDocumentEdit(textDocEdit: dict) + var bnr: number = bufnr(LspUriToFile(textDocEdit.textDocument.uri)) + if bnr == -1 + ErrMsg('Error: Text Document edit, buffer ' .. textDocEdit.textDocument.uri .. ' is not found') + return + endif + s:applyTextEdits(bnr, textDocEdit.edits) +enddef + +# interface WorkspaceEdit +def s:applyWorkspaceEdit(workspaceEdit: dict) + if workspaceEdit->has_key('documentChanges') + for change in workspaceEdit.documentChanges + if change->has_key('kind') + ErrMsg('Error: Unsupported change in workspace edit [' .. change.kind .. ']') + else + s:applyTextDocumentEdit(change) + endif + endfor + return + endif + + if !workspaceEdit->has_key('changes') + return + endif + + var save_cursor: list = getcurpos() + for [uri, changes] in items(workspaceEdit.changes) + var fname: string = LspUriToFile(uri) + var bnr: number = bufnr(fname) + if bnr == -1 + # file is already removed + continue + endif + + # interface TextEdit + s:applyTextEdits(bnr, changes) + endfor + save_cursor->setpos('.') +enddef + +# process the 'textDocument/formatting' reply from the LSP server +def s:processFormatReply(lspserver: dict, req: dict, reply: dict) + if reply.result->empty() + # nothing to format + return + endif + + # result: TextEdit[] + + var fname: string = LspUriToFile(req.params.textDocument.uri) + var bnr: number = bufnr(fname) + if bnr == -1 + # file is already removed + return + endif + + # interface TextEdit + # Apply each of the text edit operations + var save_cursor: list = getcurpos() + s:applyTextEdits(bnr, reply.result) + save_cursor->setpos('.') +enddef + +# process the 'textDocument/rename' reply from the LSP server +def s:processRenameReply(lspserver: dict, req: dict, reply: dict) + if reply.result->empty() + # nothing to rename + return + endif + + # result: WorkspaceEdit + s:applyWorkspaceEdit(reply.result) +enddef + +# interface ExecuteCommandParams +def s:executeCommand(lspserver: dict, cmd: dict) + var req = lspserver.createRequest('workspace/executeCommand') + req.params->extend(cmd) + lspserver.sendMessage(req) +enddef + +# process the 'textDocument/codeAction' reply from the LSP server +# params: interface Command[] | interface CodeAction[] +def s:processCodeActionReply(lspserver: dict, req: dict, reply: dict) + if reply.result->empty() + # no action can be performed + WarnMsg('No code action is available') + return + endif + + var actions: list> = reply.result + + var prompt: list = ['Code Actions:'] + var act: dict + for i in range(actions->len()) + act = actions[i] + var t: string = act.title->substitute('\r\n', '\\r\\n', 'g') + t = t->substitute('\n', '\\n', 'g') + prompt->add(printf("%d. %s", i + 1, t)) + endfor + var choice = inputlist(prompt) + if choice < 1 || choice > prompt->len() + return + endif + + var selAction = actions[choice - 1] + + # textDocument/codeAction can return either Command[] or CodeAction[]. + # If it is a CodeAction, it can have either an edit, a command or both. + # Edits should be executed first. + if selAction->has_key('edit') || selAction->has_key('command') + if selAction->has_key('edit') + # apply edit first + s:applyWorkspaceEdit(selAction.edit) + endif + if selAction->has_key('command') + s:executeCommand(lspserver, selAction) + endif + else + s:executeCommand(lspserver, selAction) + endif +enddef + +# process the 'workspace/executeCommand' reply from the LSP server +def s:processWorkspaceExecuteReply(lspserver: dict, req: dict, reply: dict) + if reply.result->empty() + return + endif + + # Nothing to do for the reply +enddef + +# process the 'workspace/symbol' reply from the LSP server +def s:processWorkspaceSymbolReply(lspserver: dict, req: dict, reply: dict) + if reply.result->empty() + WarnMsg('Error: Symbol not found') + return + endif + + s:showSymbols(reply.result) +enddef + +# Process various reply messages from the LSP server +export def ProcessReply(lspserver: dict, req: dict, reply: dict): void + var lsp_reply_handlers: dict = + { + 'initialize': function('s:processInitializeReply'), + 'textDocument/definition': function('s:processDefDeclReply'), + 'textDocument/declaration': function('s:processDefDeclReply'), + 'textDocument/typeDefinition': function('s:processDefDeclReply'), + 'textDocument/implementation': function('s:processDefDeclReply'), + 'textDocument/signatureHelp': function('s:processSignaturehelpReply'), + 'textDocument/completion': function('s:processCompletionReply'), + 'textDocument/hover': function('s:processHoverReply'), + 'textDocument/references': function('s:processReferencesReply'), + 'textDocument/documentHighlight': function('s:processDocHighlightReply'), + 'textDocument/documentSymbol': function('s:processDocSymbolReply'), + 'textDocument/formatting': function('s:processFormatReply'), + 'textDocument/rangeFormatting': function('s:processFormatReply'), + 'textDocument/rename': function('s:processRenameReply'), + 'textDocument/codeAction': function('s:processCodeActionReply'), + 'workspace/executeCommand': function('s:processWorkspaceExecuteReply'), + 'workspace/symbol': function('s:processWorkspaceSymbolReply') + } + + if lsp_reply_handlers->has_key(req.method) + lsp_reply_handlers[req.method](lspserver, req, reply) + else + ErrMsg("Error: Unsupported reply received from LSP server: " .. string(reply)) + endif +enddef + +# process a diagnostic notification message from the LSP server +# params: interface PublishDiagnosticsParams +def s:processDiagNotif(lspserver: dict, reply: dict): void + var fname: string = LspUriToFile(reply.params.uri) + + # 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 + endfor + + lspserver.diagsMap->extend({[fname]: diag_by_lnum}) +enddef + +# process a log notification message from the LSP server +def s:processLogMsgNotif(lspserver: dict, reply: dict) + # interface LogMessageParams + var msgType: list = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: '] + if reply.params.type == 4 + # ignore log messages from the LSP server (too chatty) + # TODO: Add a configuration to control the message level that will be + # displayed. Also store these messages and provide a command to display + # them. + return + endif + var mtype: string = 'Log: ' + if reply.params.type > 0 && reply.params.type < 5 + mtype = msgType[reply.params.type] + endif + + :echomsg 'Lsp ' .. mtype .. reply.params.message +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:processLogMsgNotif'), + 'window/logMessage': function('s:processLogMsgNotif') + } + + if lsp_notif_handlers->has_key(reply.method) + lsp_notif_handlers[reply.method](lspserver, reply) + else + ErrMsg('Error: Unsupported notification received from LSP server ' .. string(reply)) + endif +enddef + +# process the workspace/applyEdit LSP server request +def s:processApplyEditReq(lspserver: dict, request: dict) + # interface ApplyWorkspaceEditParams + if !request->has_key('params') + return + endif + var workspaceEditParams: dict = request.params + if workspaceEditParams->has_key('label') + :echomsg "Workspace edit" .. workspaceEditParams.label + endif + s:applyWorkspaceEdit(workspaceEditParams.edit) + # TODO: Need to return the proper result of the edit operation + lspserver.sendResponse(request, {'applied': v:true}, v:null) +enddef + +# process a request message from the server +export def ProcessRequest(lspserver: dict, request: dict) + var lspRequestHandlers: dict = + { + 'workspace/applyEdit': function('s:processApplyEditReq') + } + + if lspRequestHandlers->has_key(request.method) + lspRequestHandlers[request.method](lspserver, request) + else + ErrMsg('Error: Unsupported request received from LSP server ' .. + string(request)) + endif +enddef + +# process LSP server messages +export def ProcessMessages(lspserver: dict): void + while lspserver.data->len() > 0 + var idx = stridx(lspserver.data, 'Content-Length: ') + if idx == -1 + return + endif + + var len = str2nr(lspserver.data[idx + 16:]) + if len == 0 + ErrMsg("Error: Content length is zero") + return + endif + + # Header and contents are separated by '\r\n\r\n' + idx = stridx(lspserver.data, "\r\n\r\n") + if idx == -1 + ErrMsg("Error: Content separator is not found") + return + endif + + idx = idx + 4 + + if lspserver.data->len() - idx < len + # message is not fully received. Process the message after more data is + # received + return + endif + + var content = lspserver.data[idx : idx + len - 1] + var msg = content->json_decode() + + if msg->has_key('result') || msg->has_key('error') + # response message from the server + var req = lspserver.requests->get(string(msg.id)) + # Remove the corresponding stored request message + lspserver.requests->remove(string(msg.id)) + + if msg->has_key('result') + lspserver.processReply(req, msg) + else + # request failed + var emsg: string = msg.error.message + emsg ..= ', code = ' .. msg.code + if msg.error->has_key('data') + emsg = emsg .. ', data = ' .. string(msg.error.data) + endif + ErrMsg("Error: request " .. req.method .. " failed (" .. emsg .. ")") + endif + elseif msg->has_key('id') + # request message from the server + lspserver.processRequest(msg) + else + # notification message from the server + lspserver.processNotif(msg) + endif + + lspserver.data = lspserver.data[idx + len :] + endwhile +enddef + diff --git a/autoload/lsp.vim b/autoload/lsp.vim index c6f3eaf..e93007f 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -2,1340 +2,63 @@ vim9script # Vim9 LSP client -# Needs Vim 8.2.2082 and higher -if v:version < 802 || !has('patch-8.2.2082') - finish -endif - -# LSP server information -var lspServers: list> = [] - -# filetype to LSP server map -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'}) - -# Display a warning message -def WarnMsg(msg: string) - :echohl WarningMsg - :echomsg msg - :echohl None -enddef - -# Display an error message -def ErrMsg(msg: string) - :echohl Error - :echomsg msg - :echohl None -enddef - -# Return the LSP server for the a specific filetype. Returns a null dict if -# the server is not found. -def s:lspGetServer(ftype: string): dict - return ftypeServerMap->get(ftype, {}) -enddef - -# Add a LSP server for a filetype -def s:lspAddServer(ftype: string, lspserver: dict) - ftypeServerMap->extend({[ftype]: lspserver}) -enddef - -# Lsp server trace log directory -var lsp_log_dir: string -if has('unix') - lsp_log_dir = '/tmp/' -else - lsp_log_dir = $TEMP .. '\\' -endif -var lsp_server_trace: bool = v:false - -def lsp#enableServerTrace() - lsp_server_trace = v:true -enddef - -# Log a message from the LSP server. stderr is v:true for logging messages -# from the standard error and v:false for stdout. -def s:traceLog(stderr: bool, msg: string) - if !lsp_server_trace - return - endif - if stderr - writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.err', 'a') - else - writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.out', 'a') - endif -enddef - -# Empty out the LSP server trace logs -def s:clearTraceLogs() - if !lsp_server_trace - return - endif - writefile([], lsp_log_dir .. 'lsp_server.out') - writefile([], lsp_log_dir .. 'lsp_server.err') -enddef - -# Show information about all the LSP servers -def lsp#showServers() - for [ftype, lspserver] in items(ftypeServerMap) - var msg = ftype .. " " - if lspserver.running - msg ..= 'running' - else - msg ..= 'not running' - endif - msg ..= ' ' .. lspserver.path - :echomsg msg - endfor -enddef - -# Convert a LSP file URI (file://) to a Vim file name -def LspUriToFile(uri: string): string - # Replace all the %xx numbers (e.g. %20 for space) in the URI to character - var uri_decoded: string = substitute(uri, '%\(\x\x\)', - '\=nr2char(str2nr(submatch(1), 16))', 'g') - - # File URIs on MS-Windows start with file:///[a-zA-Z]:' - if uri_decoded =~? '^file:///\a:' - # MS-Windows URI - uri_decoded = uri_decoded[8:] - uri_decoded = uri_decoded->substitute('/', '\\', 'g') - else - uri_decoded = uri_decoded[7:] - endif - - return uri_decoded -enddef - -# Convert a Vim filenmae to an LSP URI (file://) -def LspFileToUri(fname: string): string - var uri: string = fnamemodify(fname, ':p') - - var on_windows: bool = v:false - if uri =~? '^\a:' - on_windows = v:true - endif - - if on_windows - # MS-Windows - uri = uri->substitute('\\', '/', 'g') - endif - - uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)', - '\=printf("%%%02x", char2nr(submatch(1)))', 'g') - - if on_windows - uri = 'file:///' .. uri - else - uri = 'file://' .. uri - endif - - return uri -enddef - -# process the 'initialize' method reply from the LSP server -def s:processInitializeReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->len() <= 0 - return - endif - - # interface 'InitializeResult' - var caps: dict = reply.result.capabilities - lspserver.caps = caps - - # TODO: Check all the buffers with filetype corresponding to this LSP server - # and then setup the below mapping for those buffers. - - # map characters that trigger signature help - if caps->has_key('signatureHelpProvider') - var triggers = caps.signatureHelpProvider.triggerCharacters - for ch in triggers - exe 'inoremap ' .. ch .. ' ' .. ch .. "=lsp#showSignature()" - endfor - endif - - # map characters that trigger insert mode completion - if caps->has_key('completionProvider') - var triggers = caps.completionProvider.triggerCharacters - for ch in triggers - exe 'inoremap ' .. ch .. ' ' .. ch .. "" - endfor - endif - - # send a "initialized" notification to server - lspserver.sendInitializedNotif() -enddef - -# process the 'textDocument/definition' / 'textDocument/declaration' / -# 'textDocument/typeDefinition' and 'textDocument/implementation' replies from -# the LSP server -def s:processDefDeclReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - WarnMsg("Error: definition is not found") - # pop the tag stack - var tagstack: dict = gettagstack() - if tagstack.length > 0 - settagstack(winnr(), {'curidx': tagstack.length}, 't') - endif - return - endif - - var result: dict = reply.result[0] - var file = LspUriToFile(result.uri) - var wid = bufwinid(file) - if wid != -1 - win_gotoid(wid) - else - exe 'split ' .. file - endif - # Set the previous cursor location mark - setpos("'`", getcurpos()) - cursor(result.range.start.line + 1, result.range.start.character + 1) - redraw! -enddef - -# process the 'textDocument/signatureHelp' reply from the LSP server -def s:processSignaturehelpReply(lspserver: dict, req: dict, reply: dict): void - var result: dict = reply.result - if result.signatures->len() <= 0 - WarnMsg('No signature help available') - return - endif - - var sig: dict = result.signatures[result.activeSignature] - var text = sig.label - var hllen = 0 - var startcol = 0 - if sig->has_key('parameters') - var params_len = sig.parameters->len() - if params_len > 0 && result.activeParameter < params_len - var label = sig.parameters[result.activeParameter].label - hllen = label->len() - startcol = text->stridx(label) - endif - endif - var popupID = popup_atcursor(text, {}) - prop_type_add('signature', {'bufnr': popupID->winbufnr(), 'highlight': 'Title'}) - if hllen > 0 - prop_add(1, startcol + 1, {'bufnr': popupID->winbufnr(), 'length': hllen, 'type': 'signature'}) - endif -enddef - -# Map LSP complete item kind to a character -def LspCompleteItemKindChar(kind: number): string - var kindMap: list = ['', - 't', # Text - 'm', # Method - 'f', # Function - 'C', # Constructor - 'F', # Field - 'v', # Variable - 'c', # Class - 'i', # Interface - 'M', # Module - 'p', # Property - 'u', # Unit - 'V', # Value - 'e', # Enum - 'k', # Keyword - 'S', # Snippet - 'C', # Color - 'f', # File - 'r', # Reference - 'F', # Folder - 'E', # EnumMember - 'd', # Contant - 's', # Struct - 'E', # Event - 'o', # Operator - 'T' # TypeParameter - ] - if kind > 25 - return '' - endif - return kindMap[kind] -enddef - -# process the 'textDocument/completion' reply from the LSP server -def s:processCompletionReply(lspserver: dict, req: dict, reply: dict): void - - var items: list> - if type(reply.result) == v:t_list - items = reply.result - else - items = reply.result.items - endif - - for item in items - var d: dict = {} - if item->has_key('insertText') - d.word = item.insertText - elseif item->has_key('textEdit') - d.word = item.textEdit.newText - else - d.word = item.label - endif - if item->has_key('kind') - # namespace CompletionItemKind - # map LSP kind to complete-item-kind - d.kind = LspCompleteItemKindChar(item.kind) - endif - if item->has_key('detail') - d.menu = item.detail - endif - if item->has_key('documentation') - d.info = item.documentation - endif - lspserver.completeItems->add(d) - endfor - - lspserver.completePending = v:false -enddef - -# process the 'textDocument/hover' reply from the LSP server -def s:processHoverReply(lspserver: dict, req: dict, reply: dict): void - if type(reply.result) == v:t_none - return - endif - - var hoverText: list - - if type(reply.result.contents) == v:t_dict - if reply.result.contents->has_key('kind') - # MarkupContent - if reply.result.contents.kind == 'plaintext' - hoverText = reply.result.contents.value->split("\n") - elseif reply.result.contents.kind == 'markdown' - hoverText = reply.result.contents.value->split("\n") - else - ErrMsg('Error: Unsupported hover contents type (' .. reply.result.contents.kind .. ')') - return - endif - elseif reply.result.contents->has_key('value') - # MarkedString - hoverText = reply.result.contents.value - else - ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')') - return - endif - elseif type(reply.result.contents) == v:t_list - # interface MarkedString[] - for e in reply.result.contents - if type(e) == v:t_string - hoverText->extend(e->split("\n")) - else - hoverText->extend(e.value->split("\n")) - endif - endfor - elseif type(reply.result.contents) == v:t_string - if reply.result.contents->empty() - return - endif - hoverText->add(reply.result.contents) - else - ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')') - return - endif - hoverText->popup_atcursor({'moved': 'word'}) -enddef - -# process the 'textDocument/references' reply from the LSP server -def s:processReferencesReply(lspserver: dict, req: dict, reply: dict): void - if type(reply.result) == v:t_none || reply.result->empty() - WarnMsg('Error: No references found') - return - endif - - # create a quickfix list with the location of the references - var locations: list> = reply.result - var qflist: list> = [] - for loc in locations - var fname: string = LspUriToFile(loc.uri) - var bnr: number = fname->bufnr() - if bnr == -1 - bnr = fname->bufadd() - bnr->bufload() - endif - var text: string = bnr->getbufline(loc.range.start.line + 1)[0] - ->trim("\t ", 1) - qflist->add({'filename': fname, - 'lnum': loc.range.start.line + 1, - 'col': loc.range.start.character + 1, - 'text': text}) - endfor - setqflist([], ' ', {'title': 'Language Server', 'items': qflist}) - var save_winid = win_getid() - copen - win_gotoid(save_winid) -enddef - -# process the 'textDocument/documentHighlight' reply from the LSP server -def s:processDocHighlightReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - return - endif - - var fname: string = LspUriToFile(req.params.textDocument.uri) - var bnr = bufnr(fname) - - for docHL in reply.result - var kind: number = docHL->get('kind', 1) - var propName: string - if kind == 2 - # Read-access - propName = 'LspReadRef' - elseif kind == 3 - # Write-access - propName = 'LspWriteRef' - else - # textual reference - propName = 'LspTextRef' - endif - prop_add(docHL.range.start.line + 1, docHL.range.start.character + 1, - {'end_lnum': docHL.range.end.line + 1, - 'end_col': docHL.range.end.character + 1, - 'bufnr': bnr, - 'type': propName}) - endfor -enddef - -# map the LSP symbol kind number to string -def LspSymbolKindToName(symkind: number): string - var symbolMap: list = ['', 'File', 'Module', 'Namespace', 'Package', - 'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum', - 'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number', - 'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct', - 'Event', 'Operator', 'TypeParameter'] - if symkind > 26 - return '' - endif - return symbolMap[symkind] -enddef - -# jump to a symbol selected in the symbols window -def lsp#jumpToSymbol() - var lnum: number = line('.') - 1 - if w:lsp_info.data[lnum]->empty() - return - endif - - var slnum: number = w:lsp_info.data[lnum].lnum - var scol: number = w:lsp_info.data[lnum].col - var fname: string = w:lsp_info.filename - - # If the file is already opened in a window, jump to it. Otherwise open it - # in another window - var wid: number = fname->bufwinid() - if wid == -1 - # Find a window showing a normal buffer and use it - for w in getwininfo() - if w.winid->getwinvar('&buftype') == '' - wid = w.winid - wid->win_gotoid() - break - endif - endfor - if wid == -1 - var symWinid: number = win_getid() - :rightbelow vnew - # retain the fixed symbol window width - win_execute(symWinid, 'vertical resize 20') - endif - - exe 'edit ' .. fname - else - wid->win_gotoid() - endif - [slnum, scol]->cursor() -enddef - -# display the list of document symbols from the LSP server in a window as a -# tree -def s:showSymbols(symTable: list>) - var symbols: dict>> - var symbolType: string - var fname: string - - for symbol in symTable - if symbol->has_key('location') - fname = LspUriToFile(symbol.location.uri) - symbolType = LspSymbolKindToName(symbol.kind) - if !symbols->has_key(symbolType) - symbols[symbolType] = [] - endif - var name: string = symbol.name - if symbol->has_key('containerName') - if symbol.containerName != '' - name ..= ' [' .. symbol.containerName .. ']' - endif - endif - symbols[symbolType]->add({'name': name, - 'lnum': symbol.location.range.start.line + 1, - 'col': symbol.location.range.start.character + 1}) - endif - endfor - - var wid: number = bufwinid('LSP-Symbols') - if wid == -1 - :20vnew LSP-Symbols - else - win_gotoid(wid) - endif - - :setlocal modifiable - :setlocal noreadonly - :silent! :%d _ - :setlocal buftype=nofile - :setlocal bufhidden=delete - :setlocal noswapfile nobuflisted - :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth - setline(1, ['# Language Server Symbols', '# ' .. fname]) - # First two lines in the buffer display comment information - var lnumMap: list> = [{}, {}] - var text: list = [] - for [symType, syms] in items(symbols) - text->extend(['', symType]) - lnumMap->extend([{}, {}]) - for s in syms - text->add(' ' .. s.name) - lnumMap->add({'lnum': s.lnum, 'col': s.col}) - endfor - endfor - append(line('$'), text) - w:lsp_info = {'filename': fname, 'data': lnumMap} - :nnoremap q :quit - :nnoremap :call lsp#jumpToSymbol() - :setlocal nomodifiable -enddef - -# process the 'textDocument/documentSymbol' reply from the LSP server -# Open a symbols window and display the symbols as a tree -def s:processDocSymbolReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - WarnMsg('No symbols are found') - return - endif - - s:showSymbols(reply.result) -enddef - -# Returns the byte number of the specified line/col position. Returns a -# zero-indexed column. 'pos' is LSP "interface position". -def s:get_line_byte_from_position(bnr: number, pos: dict): number - # LSP's line and characters are 0-indexed - # Vim's line and columns are 1-indexed - var col: number = pos.character - # When on the first character, we can ignore the difference between byte and - # character - if col > 0 - if !bnr->bufloaded() - bnr->bufload() - endif - - var ltext: list = bnr->getbufline(pos.line + 1) - if !ltext->empty() - var bidx = ltext[0]->byteidx(col) - if bidx != -1 - return bidx - endif - endif - endif - - return col -enddef - -# sort the list of edit operations in the descending order of line and column -# numbers. -# 'a': {'A': [lnum, col], 'B': [lnum, col]} -# 'b': {'A': [lnum, col], 'B': [lnum, col]} -def s:edit_sort_func(a: dict, b: dict): number - # line number - if a.A[0] != b.A[0] - return b.A[0] - a.A[0] - endif - # column number - if a.A[1] != b.A[1] - return b.A[1] - a.A[1] - endif - - return 0 -enddef - -# Replaces text in a range with new text. -# -# CAUTION: Changes in-place! -# -# 'lines': Original list of strings -# 'A': Start position; [line, col] -# 'B': End position [line, col] -# 'new_lines' A list of strings to replace the original -# -# returns the modified 'lines' -def s:set_lines(lines: list, A: list, B: list, - new_lines: list): list - var i_0: number = A[0] - - # If it extends past the end, truncate it to the end. This is because the - # way the LSP describes the range including the last newline is by - # specifying a line number after what we would call the last line. - var numlines: number = lines->len() - var i_n = [B[0], numlines - 1]->min() - - if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines - WarnMsg("set_lines: Invalid range, A = " .. string(A) - .. ", B = " .. string(B) .. ", numlines = " .. numlines - .. ", new lines = " .. string(new_lines)) - return lines - endif - - # save the prefix and suffix text before doing the replacements - var prefix: string = '' - var suffix: string = lines[i_n][B[1] :] - if A[1] > 0 - prefix = lines[i_0][0 : A[1] - 1] - endif - - var new_lines_len: number = new_lines->len() - - #echomsg 'i_0 = ' .. i_0 .. ', i_n = ' .. i_n .. ', new_lines = ' .. string(new_lines) - var n: number = i_n - i_0 + 1 - if n != new_lines_len - if n > new_lines_len - # remove the deleted lines - lines->remove(i_0, i_0 + n - new_lines_len - 1) - else - # add empty lines for newly the added lines (will be replaced with the - # actual lines below) - lines->extend(repeat([''], new_lines_len - n), i_0) - endif - endif - #echomsg "lines(1) = " .. string(lines) - - # replace the previous lines with the new lines - for i in range(new_lines_len) - lines[i_0 + i] = new_lines[i] - endfor - #echomsg "lines(2) = " .. string(lines) - - # append the suffix (if any) to the last line - if suffix != '' - var i = i_0 + new_lines_len - 1 - lines[i] = lines[i] .. suffix - endif - #echomsg "lines(3) = " .. string(lines) - - # prepend the prefix (if any) to the first line - if prefix != '' - lines[i_0] = prefix .. lines[i_0] - endif - #echomsg "lines(4) = " .. string(lines) - - return lines -enddef - -# Apply set of text edits to the specified buffer -# The text edit logic is ported from the Neovim lua implementation -def s:applyTextEdits(bnr: number, text_edits: list>): void - if text_edits->empty() - return - endif - - # if the buffer is not loaded, load it and make it a listed buffer - if !bnr->bufloaded() - bnr->bufload() - endif - bnr->setbufvar('&buflisted', v:true) - - var start_line: number = 4294967295 # 2 ^ 32 - var finish_line: number = -1 - var updated_edits: list> = [] - var start_row: number - var start_col: number - var end_row: number - var end_col: number - - # create a list of buffer positions where the edits have to be applied. - for e in text_edits - # Adjust the start and end columns for multibyte characters - start_row = e.range.start.line - start_col = s:get_line_byte_from_position(bnr, e.range.start) - end_row = e.range.end.line - end_col = s:get_line_byte_from_position(bnr, e.range.end) - start_line = [e.range.start.line, start_line]->min() - finish_line = [e.range.end.line, finish_line]->max() - - updated_edits->add({'A': [start_row, start_col], - 'B': [end_row, end_col], - 'lines': e.newText->split("\n", v:true)}) - endfor - - # Reverse sort the edit operations by descending line and column numbers so - # that they can be applied without interfering with each other. - updated_edits->sort('s:edit_sort_func') - - var lines: list = bnr->getbufline(start_line + 1, finish_line + 1) - var fix_eol: number = bnr->getbufvar('&fixeol') - var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1 - if set_eol && lines[-1]->len() != 0 - lines->add('') - endif - - #echomsg 'lines(1) = ' .. string(lines) - #echomsg updated_edits - - for e in updated_edits - var A: list = [e.A[0] - start_line, e.A[1]] - var B: list = [e.B[0] - start_line, e.B[1]] - lines = s:set_lines(lines, A, B, e.lines) - endfor - - #echomsg 'lines(2) = ' .. string(lines) - - # If the last line is empty and we need to set EOL, then remove it. - if set_eol && lines[-1]->len() == 0 - lines->remove(-1) - endif - - #echomsg 'applyTextEdits: start_line = ' .. start_line .. ', finish_line = ' .. finish_line - #echomsg 'lines = ' .. string(lines) - - # Delete all the lines that need to be modified - bnr->deletebufline(start_line + 1, finish_line + 1) - - # if the buffer is empty, appending lines before the first line adds an - # extra empty line at the end. Delete the empty line after appending the - # lines. - var dellastline: bool = v:false - if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 && - bnr->getbufline(1)[0] == '' - dellastline = v:true - endif - - # Append the updated lines - appendbufline(bnr, start_line, lines) - - if dellastline - bnr->deletebufline(bnr->getbufinfo()[0].linecount) - endif -enddef - -# interface TextDocumentEdit -def s:applyTextDocumentEdit(textDocEdit: dict) - var bnr: number = bufnr(LspUriToFile(textDocEdit.textDocument.uri)) - if bnr == -1 - ErrMsg('Error: Text Document edit, buffer ' .. textDocEdit.textDocument.uri .. ' is not found') - return - endif - s:applyTextEdits(bnr, textDocEdit.edits) -enddef - -# interface WorkspaceEdit -def s:applyWorkspaceEdit(workspaceEdit: dict) - if workspaceEdit->has_key('documentChanges') - for change in workspaceEdit.documentChanges - if change->has_key('kind') - ErrMsg('Error: Unsupported change in workspace edit [' .. change.kind .. ']') - else - s:applyTextDocumentEdit(change) - endif - endfor - return - endif - - if !workspaceEdit->has_key('changes') - return - endif - - var save_cursor: list = getcurpos() - for [uri, changes] in items(workspaceEdit.changes) - var fname: string = LspUriToFile(uri) - var bnr: number = bufnr(fname) - if bnr == -1 - # file is already removed - continue - endif - - # interface TextEdit - s:applyTextEdits(bnr, changes) - endfor - save_cursor->setpos('.') -enddef - -# process the 'textDocument/formatting' reply from the LSP server -def s:processFormatReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - # nothing to format - return - endif - - # result: TextEdit[] - - var fname: string = LspUriToFile(req.params.textDocument.uri) - var bnr: number = bufnr(fname) - if bnr == -1 - # file is already removed - return - endif - - # interface TextEdit - # Apply each of the text edit operations - var save_cursor: list = getcurpos() - s:applyTextEdits(bnr, reply.result) - save_cursor->setpos('.') -enddef - -# process the 'textDocument/rename' reply from the LSP server -def s:processRenameReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - # nothing to rename - return - endif - - # result: WorkspaceEdit - s:applyWorkspaceEdit(reply.result) -enddef - -# interface ExecuteCommandParams -def s:executeCommand(lspserver: dict, cmd: dict) - var req = lspserver.createRequest('workspace/executeCommand') - req.params->extend(cmd) - lspserver.sendMessage(req) -enddef - -# process the 'textDocument/codeAction' reply from the LSP server -# params: interface Command[] | interface CodeAction[] -def s:processCodeActionReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - # no action can be performed - WarnMsg('No code action is available') - return - endif - - var actions: list> = reply.result - - var prompt: list = ['Code Actions:'] - var act: dict - for i in range(actions->len()) - act = actions[i] - var t: string = act.title->substitute('\r\n', '\\r\\n', 'g') - t = t->substitute('\n', '\\n', 'g') - prompt->add(printf("%d. %s", i + 1, t)) - endfor - var choice = inputlist(prompt) - if choice < 1 || choice > prompt->len() - return - endif - - var selAction = actions[choice - 1] - - # textDocument/codeAction can return either Command[] or CodeAction[]. - # If it is a CodeAction, it can have either an edit, a command or both. - # Edits should be executed first. - if selAction->has_key('edit') || selAction->has_key('command') - if selAction->has_key('edit') - # apply edit first - s:applyWorkspaceEdit(selAction.edit) - endif - if selAction->has_key('command') - s:executeCommand(lspserver, selAction) - endif - else - s:executeCommand(lspserver, selAction) - endif -enddef - -# process the 'workspace/executeCommand' reply from the LSP server -def s:processWorkspaceExecuteReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - return - endif - - # Nothing to do for the reply -enddef - -# process the 'workspace/symbol' reply from the LSP server -def s:processWorkspaceSymbolReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - WarnMsg('Error: Symbol not found') - return - endif - - s:showSymbols(reply.result) -enddef - -# Process various reply messages from the LSP server -def s:processReply(lspserver: dict, req: dict, reply: dict): void - var lsp_reply_handlers: dict = - { - 'initialize': function('s:processInitializeReply'), - 'textDocument/definition': function('s:processDefDeclReply'), - 'textDocument/declaration': function('s:processDefDeclReply'), - 'textDocument/typeDefinition': function('s:processDefDeclReply'), - 'textDocument/implementation': function('s:processDefDeclReply'), - 'textDocument/signatureHelp': function('s:processSignaturehelpReply'), - 'textDocument/completion': function('s:processCompletionReply'), - 'textDocument/hover': function('s:processHoverReply'), - 'textDocument/references': function('s:processReferencesReply'), - 'textDocument/documentHighlight': function('s:processDocHighlightReply'), - 'textDocument/documentSymbol': function('s:processDocSymbolReply'), - 'textDocument/formatting': function('s:processFormatReply'), - 'textDocument/rangeFormatting': function('s:processFormatReply'), - 'textDocument/rename': function('s:processRenameReply'), - 'textDocument/codeAction': function('s:processCodeActionReply'), - 'workspace/executeCommand': function('s:processWorkspaceExecuteReply'), - 'workspace/symbol': function('s:processWorkspaceSymbolReply') - } - - if lsp_reply_handlers->has_key(req.method) - lsp_reply_handlers[req.method](lspserver, req, reply) - else - ErrMsg("Error: Unsupported reply received from LSP server: " .. string(reply)) - endif -enddef - -# process a diagnostic notification message from the LSP server -# params: interface PublishDiagnosticsParams -def s:processDiagNotif(lspserver: dict, reply: dict): void - var fname: string = LspUriToFile(reply.params.uri) - - # 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 - endfor - - diagsMap->extend({[fname]: diag_by_lnum}) -enddef - -# process a log notification message from the LSP server -def s:processLogMsgNotif(lspserver: dict, reply: dict) - # interface LogMessageParams - var msgType: list = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: '] - if reply.params.type == 4 - # ignore log messages from the LSP server (too chatty) - # TODO: Add a configuration to control the message level that will be - # displayed. Also store these messages and provide a command to display - # them. - return - endif - var mtype: string = 'Log: ' - if reply.params.type > 0 && reply.params.type < 5 - mtype = msgType[reply.params.type] - endif - - :echomsg 'Lsp ' .. mtype .. reply.params.message -enddef - -# process notification messages from the LSP server -def s:processNotif(lspserver: dict, reply: dict): void - var lsp_notif_handlers: dict = - { - 'textDocument/publishDiagnostics': function('s:processDiagNotif'), - 'window/showMessage': function('s:processLogMsgNotif'), - 'window/logMessage': function('s:processLogMsgNotif') - } - - if lsp_notif_handlers->has_key(reply.method) - lsp_notif_handlers[reply.method](lspserver, reply) - else - ErrMsg('Error: Unsupported notification received from LSP server ' .. string(reply)) - endif -enddef - -# send a response message to the server -def s:sendResponse(lspserver: dict, request: dict, result: dict, error: dict) - var resp: dict = lspserver.createResponse(request.id) - if type(result) != v:t_none - resp->extend({'result': result}) - else - resp->extend({'error': error}) - endif - lspserver.sendMessage(resp) -enddef - -# process the workspace/applyEdit LSP server request -def s:processApplyEditReq(lspserver: dict, request: dict) - # interface ApplyWorkspaceEditParams - if !request->has_key('params') - return - endif - var workspaceEditParams: dict = request.params - if workspaceEditParams->has_key('label') - :echomsg "Workspace edit" .. workspaceEditParams.label - endif - s:applyWorkspaceEdit(workspaceEditParams.edit) - # TODO: Need to return the proper result of the edit operation - lspserver.sendResponse(request, {'applied': v:true}, v:null) -enddef - -# process a request message from the server -def s:processRequest(lspserver: dict, request: dict) - var lspRequestHandlers: dict = - { - 'workspace/applyEdit': function('s:processApplyEditReq') - } - - if lspRequestHandlers->has_key(request.method) - lspRequestHandlers[request.method](lspserver, request) - else - ErrMsg('Error: Unsupported request received from LSP server ' .. - string(request)) - endif -enddef - -# process LSP server messages -def s:processMessages(lspserver: dict): void - while lspserver.data->len() > 0 - var idx = stridx(lspserver.data, 'Content-Length: ') - if idx == -1 - return - endif - - var len = str2nr(lspserver.data[idx + 16:]) - if len == 0 - ErrMsg("Error: Content length is zero") - return - endif - - # Header and contents are separated by '\r\n\r\n' - idx = stridx(lspserver.data, "\r\n\r\n") - if idx == -1 - ErrMsg("Error: Content separator is not found") - return - endif - - idx = idx + 4 - - if lspserver.data->len() - idx < len - # message is not fully received. Process the message after more data is - # received - return - endif - - var content = lspserver.data[idx : idx + len - 1] - var msg = content->json_decode() - - if msg->has_key('result') || msg->has_key('error') - # response message from the server - var req = lspserver.requests->get(string(msg.id)) - # Remove the corresponding stored request message - lspserver.requests->remove(string(msg.id)) - - if msg->has_key('result') - lspserver.processReply(req, msg) - else - # request failed - var emsg: string = msg.error.message - emsg ..= ', code = ' .. msg.code - if msg.error->has_key('data') - emsg = emsg .. ', data = ' .. string(msg.error.data) - endif - ErrMsg("Error: request " .. req.method .. " failed (" .. emsg .. ")") - endif - elseif msg->has_key('id') - # request message from the server - lspserver.processRequest(msg) - else - # notification message from the server - lspserver.processNotif(msg) - endif - - lspserver.data = lspserver.data[idx + len :] - endwhile -enddef - -# LSP server standard output handler -def lsp#output_cb(lspserver: dict, chan: channel, msg: string): void - s:traceLog(v:false, msg) - lspserver.data = lspserver.data .. msg - lspserver.processMessages() -enddef - -# LSP server error output handler -def lsp#error_cb(lspserver: dict, chan: channel, emsg: string,): void - s:traceLog(v:true, emsg) -enddef - -# LSP server exit callback -def lsp#exit_cb(lspserver: dict, job: job, status: number): void - WarnMsg("LSP server exited with status " .. status) - lspserver.job = v:none - lspserver.running = v:false - lspserver.requests = {} -enddef - -# Return the next id for a LSP server request message -def s:nextReqID(lspserver: dict): number - var id = lspserver.nextID - lspserver.nextID = id + 1 - return id -enddef - -# Send a request message to LSP server -def s:sendMessage(lspserver: dict, content: dict): void - var payload_js: string = content->json_encode() - var msg = "Content-Length: " .. payload_js->len() .. "\r\n\r\n" - var ch = lspserver.job->job_getchannel() - ch->ch_sendraw(msg) - ch->ch_sendraw(payload_js) -enddef - -# create a LSP server request message -def s:createRequest(lspserver: dict, method: string): dict - var req = {} - req.jsonrpc = '2.0' - req.id = lspserver.nextReqID() - req.method = method - req.params = {} - - # Save the request, so that the corresponding response can be processed - lspserver.requests->extend({[string(req.id)]: req}) - - return req -enddef - -# create a LSP server response message -def s:createResponse(lspserver: dict, req_id: number): dict - var resp = {} - resp.jsonrpc = '2.0' - resp.id = req_id - - return resp -enddef - -# create a LSP server notification message -def s:createNotification(lspserver: dict, notif: string): dict - var req = {} - req.jsonrpc = '2.0' - req.method = notif - req.params = {} - - return req -enddef - -# Send a "initialize" LSP request -def s:initServer(lspserver: dict) - var req = lspserver.createRequest('initialize') - - var clientCaps: dict = { - 'workspace': { - 'workspaceFolders': v:true, - 'applyEdit': v:true, - }, - 'textDocument': {}, - 'window': {}, - 'general': {} - } - - # interface 'InitializeParams' - var initparams: dict = {} - initparams.processId = getpid() - initparams.clientInfo = { - 'name': 'Vim', - 'version': string(v:versionlong), - } - initparams.rootPath = getcwd() - initparams.rootUri = LspFileToUri(getcwd()) - initparams.workspaceFolders = { - 'uri': LspFileToUri(getcwd()), - 'name': getcwd() - } - initparams.capabilities = clientCaps - req.params->extend(initparams) - - lspserver.sendMessage(req) -enddef - -# Send a "initialized" LSP notification -def s:sendInitializedNotif(lspserver: dict) - var notif: dict = lspserver.createNotification('initialized') - lspserver.sendMessage(notif) -enddef - -# Start a LSP server -def s:startServer(lspserver: dict): number - if lspserver.running - WarnMsg("LSP server for is already running") - return 0 - endif - - var cmd = [lspserver.path] - cmd->extend(lspserver.args) - - var opts = {'in_mode': 'raw', - 'out_mode': 'raw', - 'err_mode': 'raw', - 'noblock': 1, - 'out_cb': function('lsp#output_cb', [lspserver]), - 'err_cb': function('lsp#error_cb', [lspserver]), - 'exit_cb': function('lsp#exit_cb', [lspserver])} - - s:clearTraceLogs() - lspserver.data = '' - lspserver.caps = {} - lspserver.nextID = 1 - lspserver.requests = {} - lspserver.completePending = v:false - lspserver.workspaceFolders = [getcwd()] - - var job = job_start(cmd, opts) - if job->job_status() == 'fail' - ErrMsg("Error: Failed to start LSP server " .. lspserver.path) - return 1 - endif - - # wait for the LSP server to start - sleep 10m - - lspserver.job = job - lspserver.running = v:true - - lspserver.initServer() - - return 0 -enddef - -# Send a 'shutdown' request to the LSP server -def s:shutdownServer(lspserver: dict): void - var req = lspserver.createRequest('shutdown') - lspserver.sendMessage(req) -enddef - -# Send a 'exit' notification to the LSP server -def s:exitServer(lspserver: dict): void - var notif: dict = lspserver.createNotification('exit') - lspserver.sendMessage(notif) -enddef - -# Stop a LSP server -def s:stopServer(lspserver: dict): number - if !lspserver.running - WarnMsg("LSP server is not running") - return 0 - endif - - lspserver.shutdownServer() - - # Wait for the server to process the shutodwn request - sleep 1 - - lspserver.exitServer() - - lspserver.job->job_stop() - lspserver.job = v:none - lspserver.running = v:false - lspserver.requests = {} - return 0 -enddef - -# set the LSP server trace level using $/setTrace notification -def s:setTrace(lspserver: dict, traceVal: string) - var notif: dict = lspserver.createNotification('$/setTrace') - notif.params->extend({'value': traceVal}) - lspserver.sendMessage(notif) -enddef - -# Send a LSP "textDocument/didOpen" notification -def s:textdocDidOpen(lspserver: dict, bnr: number, ftype: string): void - var notif: dict = lspserver.createNotification('textDocument/didOpen') - - # interface DidOpenTextDocumentParams - # interface TextDocumentItem - var tdi = {} - tdi.uri = LspFileToUri(bufname(bnr)) - tdi.languageId = ftype - tdi.version = 1 - tdi.text = getbufline(bnr, 1, '$')->join("\n") .. "\n" - notif.params->extend({'textDocument': tdi}) - - lspserver.sendMessage(notif) -enddef - -# Send a LSP "textDocument/didClose" notification -def s:textdocDidClose(lspserver: dict, bnr: number): void - var notif: dict = lspserver.createNotification('textDocument/didClose') +import NewLspServer from './lspserver.vim' +import {WarnMsg, ErrMsg, lsp_server_trace} from './util.vim' - # interface DidCloseTextDocumentParams - # interface TextDocumentIdentifier - var tdid = {} - tdid.uri = LspFileToUri(bufname(bnr)) - notif.params->extend({'textDocument': tdid}) +# Needs Vim 8.2.2082 and higher +if v:version < 802 || !has('patch-8.2.2082') + finish +endif - lspserver.sendMessage(notif) -enddef +# LSP server information +var lspServers: list> = [] -# Return the current cursor position as a LSP position. -# LSP line and column numbers start from zero, whereas Vim line and column -# numbers start from one. The LSP column number is the character index in the -# line and not the byte index in the line. -def s:getLspPosition(): dict - var lnum: number = line('.') - 1 - #var col: number = strchars(getline('.')[: col('.') - 1]) - 1 - var col: number = col('.') - 1 - return {'line': lnum, 'character': col} -enddef +# filetype to LSP server map +var ftypeServerMap: dict> = {} -# Return the current file name and current cursor position as a LSP -# TextDocumentPositionParams structure -def s:getLspTextDocPosition(): dict> - # interface TextDocumentIdentifier - # interface Position - return {'textDocument': {'uri': LspFileToUri(@%)}, - 'position': s:getLspPosition()} -enddef +# Buffer number to LSP server map +var bufnrToServer: dict> = {} -def s:gotoDefinition(lspserver: dict): void - var req = lspserver.createRequest('textDocument/definition') +# List of diagnostics for each opened file +#var diagsMap: dict> = {} - # interface DefinitionParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) +prop_type_add('LspTextRef', {'highlight': 'Search'}) +prop_type_add('LspReadRef', {'highlight': 'DiffChange'}) +prop_type_add('LspWriteRef', {'highlight': 'DiffDelete'}) - lspserver.sendMessage(req) +# Return the LSP server for the a specific filetype. Returns a null dict if +# the server is not found. +def s:lspGetServer(ftype: string): dict + return ftypeServerMap->get(ftype, {}) enddef -def s:gotoDeclaration(lspserver: dict): void - var req = lspserver.createRequest('textDocument/declaration') - - # interface DeclarationParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - - lspserver.sendMessage(req) +# Add a LSP server for a filetype +def s:lspAddServer(ftype: string, lspserver: dict) + ftypeServerMap->extend({[ftype]: lspserver}) enddef -def s:gotoTypeDef(lspserver: dict): void - var req = lspserver.createRequest('textDocument/typeDefinition') - - # interface TypeDefinitionParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - - lspserver.sendMessage(req) +def lsp#enableServerTrace() + lsp_server_trace = v:true enddef -def s:gotoImplementation(lspserver: dict): void - var req = lspserver.createRequest('textDocument/implementation') - - # interface ImplementationParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - - lspserver.sendMessage(req) +# Show information about all the LSP servers +def lsp#showServers() + for [ftype, lspserver] in items(ftypeServerMap) + var msg = ftype .. " " + if lspserver.running + msg ..= 'running' + else + msg ..= 'not running' + endif + msg ..= ' ' .. lspserver.path + :echomsg msg + endfor enddef # Go to a definition using "textDocument/definition" LSP request def lsp#gotoDefinition() var ftype: string = &filetype - if ftype == '' + if ftype == '' || @% == '' return endif @@ -1348,25 +71,6 @@ def lsp#gotoDefinition() ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') return endif - # Check whether LSP server supports jumping to a definition - if !lspserver.caps->has_key('definitionProvider') - || !lspserver.caps.definitionProvider - ErrMsg("Error: LSP server does not support jumping to a definition") - return - endif - - var fname: string = @% - if fname == '' - return - endif - - # push the current location on to the tag stack - settagstack(winnr(), {'items': - [{'bufnr': bufnr(), - 'from': getpos('.'), - 'matchnr': 1, - 'tagname': expand('')} - ]}, 'a') lspserver.gotoDefinition() enddef @@ -1387,25 +91,6 @@ def lsp#gotoDeclaration() ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') return endif - # Check whether LSP server supports jumping to a declaration - if !lspserver.caps->has_key('declarationProvider') - || !lspserver.caps.declarationProvider - ErrMsg("Error: LSP server does not support jumping to a declaration") - return - endif - - var fname: string = @% - if fname == '' - return - endif - - # push the current location on to the tag stack - settagstack(winnr(), {'items': - [{'bufnr': bufnr(), - 'from': getpos('.'), - 'matchnr': 1, - 'tagname': expand('')} - ]}, 'a') lspserver.gotoDeclaration() enddef @@ -1426,25 +111,6 @@ def lsp#gotoTypedef() ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') return endif - # Check whether LSP server supports jumping to a type definition - if !lspserver.caps->has_key('typeDefinitionProvider') - || !lspserver.caps.typeDefinitionProvider - ErrMsg("Error: LSP server does not support jumping to a type definition") - return - endif - - var fname: string = @% - if fname == '' - return - endif - - # push the current location on to the tag stack - settagstack(winnr(), {'items': - [{'bufnr': bufnr(), - 'from': getpos('.'), - 'matchnr': 1, - 'tagname': expand('')} - ]}, 'a') lspserver.gotoTypeDef() enddef @@ -1465,44 +131,10 @@ def lsp#gotoImplementation() ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') return endif - # Check whether LSP server supports jumping to a implementation - if !lspserver.caps->has_key('implementationProvider') - || !lspserver.caps.implementationProvider - ErrMsg("Error: LSP server does not support jumping to an implementation") - return - endif - - var fname: string = @% - if fname == '' - return - endif - - # push the current location on to the tag stack - settagstack(winnr(), {'items': - [{'bufnr': bufnr(), - 'from': getpos('.'), - 'matchnr': 1, - 'tagname': expand('')} - ]}, 'a') lspserver.gotoImplementation() enddef -def s:showSignature(lspserver: dict): void - # Check whether LSP server supports signature help - if !lspserver.caps->has_key('signatureHelpProvider') - ErrMsg("Error: LSP server does not support signature help") - return - endif - - var req = lspserver.createRequest('textDocument/signatureHelp') - # interface SignatureHelpParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - - lspserver.sendMessage(req) -enddef - # Show the signature using "textDocument/signatureHelp" LSP method # Invoked from an insert-mode mapping, so return an empty string. def lsp#showSignature(): string @@ -1540,64 +172,7 @@ def lsp#bufchange_listener(bnr: number, start: number, end: number, added: numbe return endif - var notif: dict = lspserver.createNotification('textDocument/didChange') - - # interface DidChangeTextDocumentParams - # interface VersionedTextDocumentIdentifier - var vtdid: dict = {} - vtdid.uri = LspFileToUri(bufname(bnr)) - # Use Vim 'changedtick' as the LSP document version number - vtdid.version = bnr->getbufvar('changedtick') - notif.params->extend({'textDocument': vtdid}) - # interface TextDocumentContentChangeEvent - var changeset: list> - - ##### FIXME: Sending specific buffer changes to the LSP server doesn't - ##### work properly as the computed line range numbers is not correct. - ##### For now, send the entire buffer content to LSP server. - # # Range - # for change in changes - # var lines: string - # var start_lnum: number - # var end_lnum: number - # var start_col: number - # var end_col: number - # if change.added == 0 - # # lines changed - # start_lnum = change.lnum - 1 - # end_lnum = change.end - 1 - # lines = getbufline(bnr, change.lnum, change.end - 1)->join("\n") .. "\n" - # start_col = 0 - # end_col = 0 - # elseif change.added > 0 - # # lines added - # start_lnum = change.lnum - 1 - # end_lnum = change.lnum - 1 - # start_col = 0 - # end_col = 0 - # lines = getbufline(bnr, change.lnum, change.lnum + change.added - 1)->join("\n") .. "\n" - # else - # # lines removed - # start_lnum = change.lnum - 1 - # end_lnum = change.lnum + (-change.added) - 1 - # start_col = 0 - # end_col = 0 - # lines = '' - # endif - # var range: dict> = {'start': {'line': start_lnum, 'character': start_col}, 'end': {'line': end_lnum, 'character': end_col}} - # changeset->add({'range': range, 'text': lines}) - # endfor - changeset->add({'text': getbufline(bnr, 1, '$')->join("\n") .. "\n"}) - notif.params->extend({'contentChanges': changeset}) - - lspserver.sendMessage(notif) -enddef - -def s:didSaveFile(lspserver: dict): void - var notif: dict = lspserver.createNotification('textDocument/didSave') - # interface: DidSaveTextDocumentParams - notif.params->extend({'textDocument': {'uri': LspFileToUri(bufname(bnr))}}) - lspserver.sendMessage(notif) + lspserver.textdocDidChange(bnr, start, end, added, changes) enddef # A buffer is saved. Send the "textDocument/didSave" LSP notification @@ -1609,15 +184,6 @@ def s:lspSavedFile() return endif - # Check whether the LSP server supports the didSave notification - if !lspserver.caps->has_key('textDocumentSync') - || lspserver.caps.textDocumentSync->type() == v:t_number - || !lspserver.caps.textDocumentSync->has_key('save') - || !lspserver.caps.textDocumentSync.save - # LSP server doesn't support text document synchronization - return - endif - lspserver.didSaveFile() enddef @@ -1688,8 +254,8 @@ def lsp#removeFile(bnr: number): void return endif lspserver.textdocDidClose(bnr) - if diagsMap->has_key(fname) - diagsMap->remove(fname) + if lspserver.diagsMap->has_key(fname) + lspserver.diagsMap->remove(fname) endif remove(bufnrToServer, bnr) enddef @@ -1720,54 +286,7 @@ def lsp#addServer(serverList: list>) return endif - var lspserver: dict = { - path: server.path, - args: server.args, - running: v:false, - job: v:none, - data: '', - nextID: 1, - caps: {}, - requests: {}, - completePending: v:false - } - # Add the LSP server functions - lspserver->extend({ - 'startServer': function('s:startServer', [lspserver]), - 'initServer': function('s:initServer', [lspserver]), - 'stopServer': function('s:stopServer', [lspserver]), - 'shutdownServer': function('s:shutdownServer', [lspserver]), - 'exitServer': function('s:exitServer', [lspserver]), - 'setTrace': function('s:setTrace', [lspserver]), - 'nextReqID': function('s:nextReqID', [lspserver]), - 'createRequest': function('s:createRequest', [lspserver]), - 'createResponse': function('s:createResponse', [lspserver]), - 'createNotification': function('s:createNotification', [lspserver]), - 'sendResponse': function('s:sendResponse', [lspserver]), - 'sendMessage': function('s:sendMessage', [lspserver]), - 'processReply': function('s:processReply', [lspserver]), - 'processNotif': function('s:processNotif', [lspserver]), - 'processRequest': function('s:processRequest', [lspserver]), - 'processMessages': function('s:processMessages', [lspserver]), - 'textdocDidOpen': function('s:textdocDidOpen', [lspserver]), - 'textdocDidClose': function('s:textdocDidClose', [lspserver]), - 'sendInitializedNotif': function('s:sendInitializedNotif', [lspserver]), - 'getCompletion': function('s:getCompletion', [lspserver]), - 'gotoDefinition': function('s:gotoDefinition', [lspserver]), - 'gotoDeclaration': function('s:gotoDeclaration', [lspserver]), - 'gotoTypeDef': function('s:gotoTypeDef', [lspserver]), - 'gotoImplementation': function('s:gotoImplementation', [lspserver]), - 'showSignature': function('s:showSignature', [lspserver]), - 'didSaveFile': function('s:didSaveFile', [lspserver]), - 'hover': function('s:hover', [lspserver]), - 'showReferences': function('s:showReferences', [lspserver]), - 'docHighlight': function('s:docHighlight', [lspserver]), - 'showDocSymbols': function('s:showDocSymbols', [lspserver]), - 'renameSymbol': function('s:renameSymbol', [lspserver]), - 'workspaceSymbols': function('s:workspaceSymbols', [lspserver]), - 'addWorkspaceFolder': function('s:addWorkspaceFolder', [lspserver]), - 'removeWorkspaceFolder': function('s:removeWorkspaceFolder', [lspserver]) - }) + var lspserver: dict = NewLspServer(server.path, server.args) if type(server.filetype) == v:t_string s:lspAddServer(server.filetype, lspserver) @@ -1821,12 +340,27 @@ enddef # Display the diagnostic messages from the LSP server for the current buffer def lsp#showDiagnostics(): void + var ftype = &filetype + if ftype == '' + return + endif + + var lspserver: dict = s:lspGetServer(ftype) + if lspserver->empty() + ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not found') + return + endif + if !lspserver.running + ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') + return + endif + var fname: string = expand('%:p') if fname == '' return endif - if !diagsMap->has_key(fname) || diagsMap[fname]->empty() + if !lspserver.diagsMap->has_key(fname) || lspserver.diagsMap[fname]->empty() WarnMsg('No diagnostic messages found for ' .. fname) return endif @@ -1834,7 +368,7 @@ def lsp#showDiagnostics(): void var qflist: list> = [] var text: string - for [lnum, diag] in items(diagsMap[fname]) + for [lnum, diag] in items(lspserver.diagsMap[fname]) text = diag.message->substitute("\n\\+", "\n", 'g') qflist->add({'filename': fname, 'lnum': diag.range.start.line + 1, @@ -1846,27 +380,6 @@ def lsp#showDiagnostics(): void :copen enddef -def s:getCompletion(lspserver: dict): void - # Check whether LSP server supports completion - if !lspserver.caps->has_key('completionProvider') - ErrMsg("Error: LSP server does not support completion") - return - endif - - var fname = @% - if fname == '' - return - endif - - var req = lspserver.createRequest('textDocument/completion') - - # interface CompletionParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - - lspserver.sendMessage(req) -enddef - # Insert mode completion handler def lsp#completeFunc(findstart: number, base: string): any var ftype: string = &filetype @@ -1913,20 +426,6 @@ def lsp#completeFunc(findstart: number, base: string): any endif enddef -def s:hover(lspserver: dict): void - # Check whether LSP server supports getting hover information - if !lspserver.caps->has_key('hoverProvider') - || !lspserver.caps.hoverProvider - return - endif - - var req = lspserver.createRequest('textDocument/hover') - # interface HoverParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - lspserver.sendMessage(req) -enddef - # Display the hover message from the LSP server for the current cursor # location def LspHover() @@ -1951,23 +450,6 @@ def LspHover() lspserver.hover() enddef -def s:showReferences(lspserver: dict): void - # Check whether LSP server supports getting reference information - if !lspserver.caps->has_key('referencesProvider') - || !lspserver.caps.referencesProvider - ErrMsg("Error: LSP server does not support showing references") - return - endif - - var req = lspserver.createRequest('textDocument/references') - # interface ReferenceParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - req.params->extend({'context': {'includeDeclaration': v:true}}) - - lspserver.sendMessage(req) -enddef - # show symbol references def lsp#showReferences() var ftype = &filetype @@ -1993,21 +475,6 @@ def lsp#showReferences() lspserver.showReferences() enddef -def s:docHighlight(lspserver: dict): void - # Check whether LSP server supports getting highlight information - if !lspserver.caps->has_key('documentHighlightProvider') - || !lspserver.caps.documentHighlightProvider - ErrMsg("Error: LSP server does not support document highlight") - return - endif - - var req = lspserver.createRequest('textDocument/documentHighlight') - # interface DocumentHighlightParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - lspserver.sendMessage(req) -enddef - # highlight all the places where a symbol is referenced def lsp#docHighlight() var ftype = &filetype @@ -2040,21 +507,6 @@ def lsp#docHighlightClear() prop_remove({'type': 'LspWriteRef', 'all': v:true}, 1, line('$')) enddef -def s:showDocSymbols(lspserver: dict, fname: string): void - # Check whether LSP server supports getting document symbol information - if !lspserver.caps->has_key('documentSymbolProvider') - || !lspserver.caps.documentSymbolProvider - ErrMsg("Error: LSP server does not support getting list of symbols") - return - endif - - var req = lspserver.createRequest('textDocument/documentSymbol') - # interface DocumentSymbolParams - # interface TextDocumentIdentifier - req.params->extend({'textDocument': {'uri': LspFileToUri(fname)}}) - lspserver.sendMessage(req) -enddef - # open a window and display all the symbols in a file def lsp#showDocSymbols() var ftype = &filetype @@ -2102,51 +554,16 @@ def lsp#textDocFormat(range_args: number, line1: number, line2: number) return endif - # Check whether LSP server supports formatting documents - if !lspserver.caps->has_key('documentFormattingProvider') - || !lspserver.caps.documentFormattingProvider - ErrMsg("Error: LSP server does not support formatting documents") - return - endif - var fname = @% if fname == '' return endif - var cmd: string if range_args > 0 - cmd = 'textDocument/rangeFormatting' - else - cmd = 'textDocument/formatting' - endif - var req = lspserver.createRequest(cmd) - - # interface DocumentFormattingParams - # interface TextDocumentIdentifier - req.params->extend({'textDocument': {'uri': LspFileToUri(fname)}}) - var tabsz: number - if &sts > 0 - tabsz = &sts - elseif &sts < 0 - tabsz = &shiftwidth + lspserver.textDocFormat(fname, v:true, line1, line2) else - tabsz = &tabstop - endif - # interface FormattingOptions - var fmtopts: dict = { - tabSize: tabsz, - insertSpaces: &expandtab ? v:true : v:false, - } - req.params->extend({'options': fmtopts}) - if range_args > 0 - var r: dict> = { - 'start': {'line': line1 - 1, 'character': 0}, - 'end': {'line': line2, 'character': 0}} - req.params->extend({'range': r}) + lspserver.textDocFormat(fname, v:false, 0, 0) endif - - lspserver.sendMessage(req) enddef # TODO: Add support for textDocument.onTypeFormatting? @@ -2155,28 +572,6 @@ enddef # Display all the locations where the current symbol is called from. # Uses LSP "callHierarchy/incomingCalls" request def lsp#incomingCalls() - var ftype = &filetype - if ftype == '' - return - endif - - var lspserver: dict = s:lspGetServer(ftype) - if lspserver->empty() - ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not found') - return - endif - if !lspserver.running - ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') - return - endif - - # Check whether LSP server supports document highlight - if !lspserver.caps->has_key('documentHighlightProvider') - || !lspserver.caps.documentHighlightProvider - ErrMsg("Error: LSP server does not support document highlight") - return - endif - :echomsg 'Error: Not implemented yet' enddef @@ -2186,22 +581,6 @@ def lsp#outgoingCalls() :echomsg 'Error: Not implemented yet' enddef -def s:renameSymbol(lspserver: dict, newName: string) - # Check whether LSP server supports rename operation - if !lspserver.caps->has_key('renameProvider') - || !lspserver.caps.renameProvider - ErrMsg("Error: LSP server does not support rename operation") - return - endif - - var req = lspserver.createRequest('textDocument/rename') - # interface RenameParams - # interface TextDocumentPositionParams - req.params->extend(s:getLspTextDocPosition()) - req.params->extend({'newName': newName}) - lspserver.sendMessage(req) -enddef - # Rename a symbol # Uses LSP "textDocument/rename" request def lsp#rename() @@ -2251,48 +630,12 @@ def lsp#codeAction() return endif - # Check whether LSP server supports code action operation - if !lspserver.caps->has_key('codeActionProvider') - || !lspserver.caps.codeActionProvider - ErrMsg("Error: LSP server does not support code action operation") - return - endif - var fname = @% if fname == '' return endif - var req = lspserver.createRequest('textDocument/codeAction') - - # interface CodeActionParams - req.params->extend({'textDocument': {'uri': LspFileToUri(fname)}}) - var r: dict> = { - 'start': {'line': line('.') - 1, 'character': col('.') - 1}, - 'end': {'line': line('.') - 1, 'character': col('.') - 1}} - req.params->extend({'range': r}) - var diag: list> = [] - var lnum = line('.') - fname = fnamemodify(fname, ':p') - if diagsMap->has_key(fname) && diagsMap[fname]->has_key(lnum) - diag->add(diagsMap[fname][lnum]) - endif - req.params->extend({'context': {'diagnostics': diag}}) - - lspserver.sendMessage(req) -enddef - -def s:workspaceSymbols(lspserver: dict, sym: string) - # Check whether the LSP server supports listing workspace symbols - if !lspserver.caps->has_key('workspaceSymbolProvider') - || !lspserver.caps.workspaceSymbolProvider - ErrMsg("Error: LSP server does not support listing workspace symbols") - return - endif - - var req = lspserver.createRequest('workspace/symbol') - req.params->extend({'query': sym}) - lspserver.sendMessage(req) + lspserver.codeAction(fname) enddef # Perform a workspace wide symbol lookup @@ -2346,53 +689,6 @@ def lsp#listWorkspaceFolders() echomsg 'Workspace Folders: ' .. string(lspserver.workspaceFolders) enddef -def s:addWorkspaceFolder(lspserver: dict, dirName: string): void - if !lspserver.caps->has_key('workspace') - || !lspserver.caps.workspace->has_key('workspaceFolders') - || !lspserver.caps.workspace.workspaceFolders->has_key('supported') - || !lspserver.caps.workspace.workspaceFolders.supported - ErrMsg('Error: LSP server does not support workspace folders') - return - endif - - if lspserver.workspaceFolders->index(dirName) != -1 - ErrMsg('Error: ' .. dirName .. ' is already part of this workspace') - return - endif - - var notif: dict = - lspserver.createNotification('workspace/didChangeWorkspaceFolders') - # interface DidChangeWorkspaceFoldersParams - notif.params->extend({'event': {'added': [dirName], 'removed': []}}) - lspserver.sendMessage(notif) - - lspserver.workspaceFolders->add(dirName) -enddef - -def s:removeWorkspaceFolder(lspserver: dict, dirName: string): void - if !lspserver.caps->has_key('workspace') - || !lspserver.caps.workspace->has_key('workspaceFolders') - || !lspserver.caps.workspace.workspaceFolders->has_key('supported') - || !lspserver.caps.workspace.workspaceFolders.supported - ErrMsg('Error: LSP server does not support workspace folders') - return - endif - - var idx: number = lspserver.workspaceFolders->index(dirName) - if idx == -1 - ErrMsg('Error: ' .. dirName .. ' is not currently part of this workspace') - return - endif - - var notif: dict = - lspserver.createNotification('workspace/didChangeWorkspaceFolders') - # interface DidChangeWorkspaceFoldersParams - notif.params->extend({'event': {'added': [], 'removed': [dirName]}}) - lspserver.sendMessage(notif) - - lspserver.workspaceFolders->remove(idx) -enddef - # Add a workspace folder. Default is to use the current folder. def lsp#addWorkspaceFolder(dirArg: string) var ftype = &filetype diff --git a/autoload/lspserver.vim b/autoload/lspserver.vim new file mode 100644 index 0000000..8340879 --- /dev/null +++ b/autoload/lspserver.vim @@ -0,0 +1,715 @@ +vim9script + +# LSP server functions + +import {ProcessReply, ProcessNotif, ProcessRequest, ProcessMessages} from './handlers.vim' +import {WarnMsg, ErrMsg, ClearTraceLogs, TraceLog, LspUriToFile, LspFileToUri} from './util.vim' + +# LSP server standard output handler +def s:output_cb(lspserver: dict, chan: channel, msg: string): void + TraceLog(v:false, msg) + lspserver.data = lspserver.data .. msg + lspserver.processMessages() +enddef + +# LSP server error output handler +def s:error_cb(lspserver: dict, chan: channel, emsg: string,): void + TraceLog(v:true, emsg) +enddef + +# LSP server exit callback +def s:exit_cb(lspserver: dict, job: job, status: number): void + WarnMsg("LSP server exited with status " .. status) + lspserver.job = v:none + lspserver.running = v:false + lspserver.requests = {} +enddef + +# Start a LSP server +def s:startServer(lspserver: dict): number + if lspserver.running + WarnMsg("LSP server for is already running") + return 0 + endif + + var cmd = [lspserver.path] + cmd->extend(lspserver.args) + + var opts = {in_mode: 'raw', + out_mode: 'raw', + err_mode: 'raw', + noblock: 1, + out_cb: function('s:output_cb', [lspserver]), + err_cb: function('s:error_cb', [lspserver]), + exit_cb: function('s:exit_cb', [lspserver])} + + ClearTraceLogs() + lspserver.data = '' + lspserver.caps = {} + lspserver.nextID = 1 + lspserver.requests = {} + lspserver.completePending = v:false + lspserver.workspaceFolders = [getcwd()] + + var job = job_start(cmd, opts) + if job->job_status() == 'fail' + ErrMsg("Error: Failed to start LSP server " .. lspserver.path) + return 1 + endif + + # wait for the LSP server to start + sleep 10m + + lspserver.job = job + lspserver.running = v:true + + lspserver.initServer() + + return 0 +enddef + +# Send a "initialize" LSP request +def s:initServer(lspserver: dict) + var req = lspserver.createRequest('initialize') + + var clientCaps: dict = { + workspace: { + workspaceFolders: v:true, + applyEdit: v:true, + }, + textDocument: {}, + window: {}, + general: {} + } + + # interface 'InitializeParams' + var initparams: dict = {} + initparams.processId = getpid() + initparams.clientInfo = { + name: 'Vim', + version: string(v:versionlong), + } + initparams.rootPath = getcwd() + initparams.rootUri = LspFileToUri(getcwd()) + initparams.workspaceFolders = { + uri: LspFileToUri(getcwd()), + name: getcwd() + } + initparams.capabilities = clientCaps + req.params->extend(initparams) + + lspserver.sendMessage(req) +enddef + +# Send a "initialized" LSP notification +def s:sendInitializedNotif(lspserver: dict) + var notif: dict = lspserver.createNotification('initialized') + lspserver.sendMessage(notif) +enddef + +# Send a 'shutdown' request to the LSP server +def s:shutdownServer(lspserver: dict): void + var req = lspserver.createRequest('shutdown') + lspserver.sendMessage(req) +enddef + +# Send a 'exit' notification to the LSP server +def s:exitServer(lspserver: dict): void + var notif: dict = lspserver.createNotification('exit') + lspserver.sendMessage(notif) +enddef + +# Stop a LSP server +def s:stopServer(lspserver: dict): number + if !lspserver.running + WarnMsg("LSP server is not running") + return 0 + endif + + lspserver.shutdownServer() + + # Wait for the server to process the shutodwn request + sleep 1 + + lspserver.exitServer() + + lspserver.job->job_stop() + lspserver.job = v:none + lspserver.running = v:false + lspserver.requests = {} + return 0 +enddef + +# set the LSP server trace level using $/setTrace notification +def s:setTrace(lspserver: dict, traceVal: string) + var notif: dict = lspserver.createNotification('$/setTrace') + notif.params->extend({value: traceVal}) + lspserver.sendMessage(notif) +enddef + +# Return the next id for a LSP server request message +def s:nextReqID(lspserver: dict): number + var id = lspserver.nextID + lspserver.nextID = id + 1 + return id +enddef + +# create a LSP server request message +def s:createRequest(lspserver: dict, method: string): dict + var req = {} + req.jsonrpc = '2.0' + req.id = lspserver.nextReqID() + req.method = method + req.params = {} + + # Save the request, so that the corresponding response can be processed + lspserver.requests->extend({[string(req.id)]: req}) + + return req +enddef + +# create a LSP server response message +def s:createResponse(lspserver: dict, req_id: number): dict + var resp = {} + resp.jsonrpc = '2.0' + resp.id = req_id + + return resp +enddef + +# create a LSP server notification message +def s:createNotification(lspserver: dict, notif: string): dict + var req = {} + req.jsonrpc = '2.0' + req.method = notif + req.params = {} + + return req +enddef + +# send a response message to the server +def s:sendResponse(lspserver: dict, request: dict, result: dict, error: dict) + var resp: dict = lspserver.createResponse(request.id) + if type(result) != v:t_none + resp->extend({result: result}) + else + resp->extend({error: error}) + endif + lspserver.sendMessage(resp) +enddef + +# Send a request message to LSP server +def s:sendMessage(lspserver: dict, content: dict): void + var payload_js: string = content->json_encode() + var msg = "Content-Length: " .. payload_js->len() .. "\r\n\r\n" + var ch = lspserver.job->job_getchannel() + ch->ch_sendraw(msg) + ch->ch_sendraw(payload_js) +enddef + +# Send a LSP "textDocument/didOpen" notification +def s:textdocDidOpen(lspserver: dict, bnr: number, ftype: string): void + var notif: dict = lspserver.createNotification('textDocument/didOpen') + + # interface DidOpenTextDocumentParams + # interface TextDocumentItem + var tdi = {} + tdi.uri = LspFileToUri(bufname(bnr)) + tdi.languageId = ftype + tdi.version = 1 + tdi.text = getbufline(bnr, 1, '$')->join("\n") .. "\n" + notif.params->extend({textDocument: tdi}) + + lspserver.sendMessage(notif) +enddef + +# Send a LSP "textDocument/didClose" notification +def s:textdocDidClose(lspserver: dict, bnr: number): void + var notif: dict = lspserver.createNotification('textDocument/didClose') + + # interface DidCloseTextDocumentParams + # interface TextDocumentIdentifier + var tdid = {} + tdid.uri = LspFileToUri(bufname(bnr)) + notif.params->extend({textDocument: tdid}) + + lspserver.sendMessage(notif) +enddef + +# Send a LSP "textDocument/didChange" notification +def s:textdocDidChange(lspserver: dict, bnr: number, start: number, + end: number, added: number, + changes: list>): void + var notif: dict = lspserver.createNotification('textDocument/didChange') + + # interface DidChangeTextDocumentParams + # interface VersionedTextDocumentIdentifier + var vtdid: dict = {} + vtdid.uri = LspFileToUri(bufname(bnr)) + # Use Vim 'changedtick' as the LSP document version number + vtdid.version = bnr->getbufvar('changedtick') + notif.params->extend({textDocument: vtdid}) + # interface TextDocumentContentChangeEvent + var changeset: list> + + ##### FIXME: Sending specific buffer changes to the LSP server doesn't + ##### work properly as the computed line range numbers is not correct. + ##### For now, send the entire buffer content to LSP server. + # # Range + # for change in changes + # var lines: string + # var start_lnum: number + # var end_lnum: number + # var start_col: number + # var end_col: number + # if change.added == 0 + # # lines changed + # start_lnum = change.lnum - 1 + # end_lnum = change.end - 1 + # lines = getbufline(bnr, change.lnum, change.end - 1)->join("\n") .. "\n" + # start_col = 0 + # end_col = 0 + # elseif change.added > 0 + # # lines added + # start_lnum = change.lnum - 1 + # end_lnum = change.lnum - 1 + # start_col = 0 + # end_col = 0 + # lines = getbufline(bnr, change.lnum, change.lnum + change.added - 1)->join("\n") .. "\n" + # else + # # lines removed + # start_lnum = change.lnum - 1 + # end_lnum = change.lnum + (-change.added) - 1 + # start_col = 0 + # end_col = 0 + # lines = '' + # endif + # var range: dict> = {'start': {'line': start_lnum, 'character': start_col}, 'end': {'line': end_lnum, 'character': end_col}} + # changeset->add({'range': range, 'text': lines}) + # endfor + changeset->add({text: getbufline(bnr, 1, '$')->join("\n") .. "\n"}) + notif.params->extend({contentChanges: changeset}) + + lspserver.sendMessage(notif) +enddef + +# Return the current cursor position as a LSP position. +# LSP line and column numbers start from zero, whereas Vim line and column +# numbers start from one. The LSP column number is the character index in the +# line and not the byte index in the line. +def s:getLspPosition(): dict + var lnum: number = line('.') - 1 + #var col: number = strchars(getline('.')[: col('.') - 1]) - 1 + var col: number = col('.') - 1 + return {line: lnum, character: col} +enddef + +# Return the current file name and current cursor position as a LSP +# TextDocumentPositionParams structure +def s:getLspTextDocPosition(): dict> + # interface TextDocumentIdentifier + # interface Position + return {textDocument: {uri: LspFileToUri(@%)}, + position: s:getLspPosition()} +enddef + +def s:getCompletion(lspserver: dict): void + # Check whether LSP server supports completion + if !lspserver.caps->has_key('completionProvider') + ErrMsg("Error: LSP server does not support completion") + return + endif + + var fname = @% + if fname == '' + return + endif + + var req = lspserver.createRequest('textDocument/completion') + + # interface CompletionParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + + lspserver.sendMessage(req) +enddef + +# push the current location on to the tag stack +def s:pushCursorToTagStack() + settagstack(winnr(), {items: [ + { + bufnr: bufnr(), + from: getpos('.'), + matchnr: 1, + tagname: expand('') + }]}, 'a') +enddef + +def s:gotoDefinition(lspserver: dict): void + # Check whether LSP server supports jumping to a definition + if !lspserver.caps->has_key('definitionProvider') + || !lspserver.caps.definitionProvider + ErrMsg("Error: LSP server does not support jumping to a definition") + return + endif + + s:pushCursorToTagStack() + var req = lspserver.createRequest('textDocument/definition') + # interface DefinitionParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + lspserver.sendMessage(req) +enddef + +def s:gotoDeclaration(lspserver: dict): void + # Check whether LSP server supports jumping to a declaration + if !lspserver.caps->has_key('declarationProvider') + || !lspserver.caps.declarationProvider + ErrMsg("Error: LSP server does not support jumping to a declaration") + return + endif + + s:pushCursorToTagStack() + var req = lspserver.createRequest('textDocument/declaration') + + # interface DeclarationParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + + lspserver.sendMessage(req) +enddef + +def s:gotoTypeDef(lspserver: dict): void + # Check whether LSP server supports jumping to a type definition + if !lspserver.caps->has_key('typeDefinitionProvider') + || !lspserver.caps.typeDefinitionProvider + ErrMsg("Error: LSP server does not support jumping to a type definition") + return + endif + + s:pushCursorToTagStack() + var req = lspserver.createRequest('textDocument/typeDefinition') + + # interface TypeDefinitionParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + + lspserver.sendMessage(req) +enddef + +def s:gotoImplementation(lspserver: dict): void + # Check whether LSP server supports jumping to a implementation + if !lspserver.caps->has_key('implementationProvider') + || !lspserver.caps.implementationProvider + ErrMsg("Error: LSP server does not support jumping to an implementation") + return + endif + + s:pushCursorToTagStack() + var req = lspserver.createRequest('textDocument/implementation') + + # interface ImplementationParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + + lspserver.sendMessage(req) +enddef + +def s:showSignature(lspserver: dict): void + # Check whether LSP server supports signature help + if !lspserver.caps->has_key('signatureHelpProvider') + ErrMsg("Error: LSP server does not support signature help") + return + endif + + var req = lspserver.createRequest('textDocument/signatureHelp') + # interface SignatureHelpParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + + lspserver.sendMessage(req) +enddef + +def s:didSaveFile(lspserver: dict): void + # Check whether the LSP server supports the didSave notification + if !lspserver.caps->has_key('textDocumentSync') + || lspserver.caps.textDocumentSync->type() == v:t_number + || !lspserver.caps.textDocumentSync->has_key('save') + || !lspserver.caps.textDocumentSync.save + # LSP server doesn't support text document synchronization + return + endif + + var notif: dict = lspserver.createNotification('textDocument/didSave') + # interface: DidSaveTextDocumentParams + notif.params->extend({textDocument: {uri: LspFileToUri(bufname(bnr))}}) + lspserver.sendMessage(notif) +enddef + +def s:hover(lspserver: dict): void + # Check whether LSP server supports getting hover information + if !lspserver.caps->has_key('hoverProvider') + || !lspserver.caps.hoverProvider + return + endif + + var req = lspserver.createRequest('textDocument/hover') + # interface HoverParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + lspserver.sendMessage(req) +enddef + +def s:showReferences(lspserver: dict): void + # Check whether LSP server supports getting reference information + if !lspserver.caps->has_key('referencesProvider') + || !lspserver.caps.referencesProvider + ErrMsg("Error: LSP server does not support showing references") + return + endif + + var req = lspserver.createRequest('textDocument/references') + # interface ReferenceParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + req.params->extend({context: {includeDeclaration: v:true}}) + + lspserver.sendMessage(req) +enddef + +def s:docHighlight(lspserver: dict): void + # Check whether LSP server supports getting highlight information + if !lspserver.caps->has_key('documentHighlightProvider') + || !lspserver.caps.documentHighlightProvider + ErrMsg("Error: LSP server does not support document highlight") + return + endif + + var req = lspserver.createRequest('textDocument/documentHighlight') + # interface DocumentHighlightParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + lspserver.sendMessage(req) +enddef + +def s:showDocSymbols(lspserver: dict, fname: string): void + # Check whether LSP server supports getting document symbol information + if !lspserver.caps->has_key('documentSymbolProvider') + || !lspserver.caps.documentSymbolProvider + ErrMsg("Error: LSP server does not support getting list of symbols") + return + endif + + var req = lspserver.createRequest('textDocument/documentSymbol') + # interface DocumentSymbolParams + # interface TextDocumentIdentifier + req.params->extend({textDocument: {uri: LspFileToUri(fname)}}) + lspserver.sendMessage(req) +enddef + +def s:textDocFormat(lspserver: dict, fname: string, rangeFormat: bool, + start_lnum: number, end_lnum: number) + # Check whether LSP server supports formatting documents + if !lspserver.caps->has_key('documentFormattingProvider') + || !lspserver.caps.documentFormattingProvider + ErrMsg("Error: LSP server does not support formatting documents") + return + endif + + var cmd: string + if rangeFormat + cmd = 'textDocument/rangeFormatting' + else + cmd = 'textDocument/formatting' + endif + var req = lspserver.createRequest(cmd) + + # interface DocumentFormattingParams + # interface TextDocumentIdentifier + req.params->extend({textDocument: {uri: LspFileToUri(fname)}}) + var tabsz: number + if &sts > 0 + tabsz = &sts + elseif &sts < 0 + tabsz = &shiftwidth + else + tabsz = &tabstop + endif + # interface FormattingOptions + var fmtopts: dict = { + tabSize: tabsz, + insertSpaces: &expandtab ? v:true : v:false, + } + req.params->extend({options: fmtopts}) + if rangeFormat + var r: dict> = { + start: {line: start_lnum - 1, character: 0}, + end: {line: end_lnum, character: 0}} + req.params->extend({range: r}) + endif + + lspserver.sendMessage(req) +enddef + +def s:renameSymbol(lspserver: dict, newName: string) + # Check whether LSP server supports rename operation + if !lspserver.caps->has_key('renameProvider') + || !lspserver.caps.renameProvider + ErrMsg("Error: LSP server does not support rename operation") + return + endif + + var req = lspserver.createRequest('textDocument/rename') + # interface RenameParams + # interface TextDocumentPositionParams + req.params->extend(s:getLspTextDocPosition()) + req.params->extend({newName: newName}) + lspserver.sendMessage(req) +enddef + +def s:codeAction(lspserver: dict, fname_arg: string) + # Check whether LSP server supports code action operation + if !lspserver.caps->has_key('codeActionProvider') + || !lspserver.caps.codeActionProvider + ErrMsg("Error: LSP server does not support code action operation") + return + endif + + var req = lspserver.createRequest('textDocument/codeAction') + + # interface CodeActionParams + var fname: string = fnamemodify(fname_arg, ':p') + req.params->extend({textDocument: {uri: LspFileToUri(fname)}}) + var r: dict> = { + start: {line: line('.') - 1, character: col('.') - 1}, + end: {line: line('.') - 1, character: col('.') - 1}} + 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]) + endif + req.params->extend({context: {diagnostics: diag}}) + + lspserver.sendMessage(req) +enddef + +def s:workspaceSymbols(lspserver: dict, sym: string) + # Check whether the LSP server supports listing workspace symbols + if !lspserver.caps->has_key('workspaceSymbolProvider') + || !lspserver.caps.workspaceSymbolProvider + ErrMsg("Error: LSP server does not support listing workspace symbols") + return + endif + + var req = lspserver.createRequest('workspace/symbol') + req.params->extend({query: sym}) + lspserver.sendMessage(req) +enddef + +def s:addWorkspaceFolder(lspserver: dict, dirName: string): void + if !lspserver.caps->has_key('workspace') + || !lspserver.caps.workspace->has_key('workspaceFolders') + || !lspserver.caps.workspace.workspaceFolders->has_key('supported') + || !lspserver.caps.workspace.workspaceFolders.supported + ErrMsg('Error: LSP server does not support workspace folders') + return + endif + + if lspserver.workspaceFolders->index(dirName) != -1 + ErrMsg('Error: ' .. dirName .. ' is already part of this workspace') + return + endif + + var notif: dict = + lspserver.createNotification('workspace/didChangeWorkspaceFolders') + # interface DidChangeWorkspaceFoldersParams + notif.params->extend({event: {added: [dirName], removed: []}}) + lspserver.sendMessage(notif) + + lspserver.workspaceFolders->add(dirName) +enddef + +def s:removeWorkspaceFolder(lspserver: dict, dirName: string): void + if !lspserver.caps->has_key('workspace') + || !lspserver.caps.workspace->has_key('workspaceFolders') + || !lspserver.caps.workspace.workspaceFolders->has_key('supported') + || !lspserver.caps.workspace.workspaceFolders.supported + ErrMsg('Error: LSP server does not support workspace folders') + return + endif + + var idx: number = lspserver.workspaceFolders->index(dirName) + if idx == -1 + ErrMsg('Error: ' .. dirName .. ' is not currently part of this workspace') + return + endif + + var notif: dict = + lspserver.createNotification('workspace/didChangeWorkspaceFolders') + # interface DidChangeWorkspaceFoldersParams + notif.params->extend({event: {added: [], removed: [dirName]}}) + lspserver.sendMessage(notif) + + lspserver.workspaceFolders->remove(idx) +enddef + +export def NewLspServer(path: string, args: list): dict + var lspserver: dict = { + path: path, + args: args, + running: v:false, + job: v:none, + data: '', + nextID: 1, + caps: {}, + requests: {}, + completePending: v:false, + diagsMap: {} + } + # Add the LSP server functions + lspserver->extend({ + startServer: function('s:startServer', [lspserver]), + initServer: function('s:initServer', [lspserver]), + stopServer: function('s:stopServer', [lspserver]), + shutdownServer: function('s:shutdownServer', [lspserver]), + exitServer: function('s:exitServer', [lspserver]), + setTrace: function('s:setTrace', [lspserver]), + nextReqID: function('s:nextReqID', [lspserver]), + createRequest: function('s:createRequest', [lspserver]), + createResponse: function('s:createResponse', [lspserver]), + createNotification: function('s:createNotification', [lspserver]), + sendResponse: function('s:sendResponse', [lspserver]), + sendMessage: function('s:sendMessage', [lspserver]), + processReply: function('ProcessReply', [lspserver]), + processNotif: function('ProcessNotif', [lspserver]), + processRequest: function('ProcessRequest', [lspserver]), + processMessages: function('ProcessMessages', [lspserver]), + textdocDidOpen: function('s:textdocDidOpen', [lspserver]), + textdocDidClose: function('s:textdocDidClose', [lspserver]), + textdocDidChange: function('s:textdocDidChange', [lspserver]), + sendInitializedNotif: function('s:sendInitializedNotif', [lspserver]), + getCompletion: function('s:getCompletion', [lspserver]), + gotoDefinition: function('s:gotoDefinition', [lspserver]), + gotoDeclaration: function('s:gotoDeclaration', [lspserver]), + gotoTypeDef: function('s:gotoTypeDef', [lspserver]), + gotoImplementation: function('s:gotoImplementation', [lspserver]), + showSignature: function('s:showSignature', [lspserver]), + didSaveFile: function('s:didSaveFile', [lspserver]), + hover: function('s:hover', [lspserver]), + showReferences: function('s:showReferences', [lspserver]), + docHighlight: function('s:docHighlight', [lspserver]), + showDocSymbols: function('s:showDocSymbols', [lspserver]), + textDocFormat: function('s:textDocFormat', [lspserver]), + renameSymbol: function('s:renameSymbol', [lspserver]), + codeAction: function('s:codeAction', [lspserver]), + workspaceSymbols: function('s:workspaceSymbols', [lspserver]), + addWorkspaceFolder: function('s:addWorkspaceFolder', [lspserver]), + removeWorkspaceFolder: function('s:removeWorkspaceFolder', [lspserver]) + }) + + return lspserver +enddef + +# vim: shiftwidth=2 softtabstop=2 diff --git a/autoload/util.vim b/autoload/util.vim new file mode 100644 index 0000000..7a75046 --- /dev/null +++ b/autoload/util.vim @@ -0,0 +1,91 @@ +vim9script + +# Display a warning message +export def WarnMsg(msg: string) + :echohl WarningMsg + :echomsg msg + :echohl None +enddef + +# Display an error message +export def ErrMsg(msg: string) + :echohl Error + :echomsg msg + :echohl None +enddef + +# Lsp server trace log directory +var lsp_log_dir: string +if has('unix') + lsp_log_dir = '/tmp/' +else + lsp_log_dir = $TEMP .. '\\' +endif +export var lsp_server_trace: bool = v:false + +# Log a message from the LSP server. stderr is v:true for logging messages +# from the standard error and v:false for stdout. +export def TraceLog(stderr: bool, msg: string) + if !lsp_server_trace + return + endif + if stderr + writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.err', 'a') + else + writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.out', 'a') + endif +enddef + +# Empty out the LSP server trace logs +export def ClearTraceLogs() + if !lsp_server_trace + return + endif + writefile([], lsp_log_dir .. 'lsp_server.out') + writefile([], lsp_log_dir .. 'lsp_server.err') +enddef + +# Convert a LSP file URI (file://) to a Vim file name +export def LspUriToFile(uri: string): string + # Replace all the %xx numbers (e.g. %20 for space) in the URI to character + var uri_decoded: string = substitute(uri, '%\(\x\x\)', + '\=nr2char(str2nr(submatch(1), 16))', 'g') + + # File URIs on MS-Windows start with file:///[a-zA-Z]:' + if uri_decoded =~? '^file:///\a:' + # MS-Windows URI + uri_decoded = uri_decoded[8:] + uri_decoded = uri_decoded->substitute('/', '\\', 'g') + else + uri_decoded = uri_decoded[7:] + endif + + return uri_decoded +enddef + +# Convert a Vim filenmae to an LSP URI (file://) +export def LspFileToUri(fname: string): string + var uri: string = fnamemodify(fname, ':p') + + var on_windows: bool = v:false + if uri =~? '^\a:' + on_windows = v:true + endif + + if on_windows + # MS-Windows + uri = uri->substitute('\\', '/', 'g') + endif + + uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)', + '\=printf("%%%02x", char2nr(submatch(1)))', 'g') + + if on_windows + uri = 'file:///' .. uri + else + uri = 'file://' .. uri + endif + + return uri +enddef + -- 2.48.1