From 7f108d35c513ab6c6ab17dab95ba0f2e16404913 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Sun, 6 Dec 2020 21:00:52 -0800 Subject: [PATCH] Initial version --- README.md | 6 +- autoload/lsp.vim | 327 +++++++++++++++++++++++++++++++++++++++++++++++ doc/lsp.txt | 0 plugin/lsp.vim | 14 ++ 4 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 autoload/lsp.vim create mode 100644 doc/lsp.txt create mode 100644 plugin/lsp.vim diff --git a/README.md b/README.md index e5e2546..a19266f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# lsp -Language Server Protocol (LSP) plugin for Vim9 +# LSP vim9 plugin + +Language Server Protocol (LSP) plugin for Vim9. + diff --git a/autoload/lsp.vim b/autoload/lsp.vim new file mode 100644 index 0000000..897650b --- /dev/null +++ b/autoload/lsp.vim @@ -0,0 +1,327 @@ +vim9script + +# Vim LSP client + +var lsp_servers: dict> = {} + +def lsp#process_reply(req: dict, reply: dict): void + if req.method == 'textDocument/definition' || req.method == 'textDocument/declaration' + if reply.result->len() == 0 + echomsg "Error: definition is not found" + else + var result: dict = reply.result[0] + var file = result.uri[7:] + var wid = bufwinid(file) + if wid != -1 + win_gotoid(wid) + else + exe 'split ' .. file + endif + cursor(result.range.start.line + 1, result.range.start.character + 1) + redraw! + endif + endif +enddef + +def lsp#process_server_msg(ftype: string): void + while lsp_servers[ftype].data->len() > 0 + var idx = stridx(lsp_servers[ftype].data, 'Content-Length: ') + if idx == -1 + return + endif + + var len = str2nr(lsp_servers[ftype].data[idx + 16:]) + if len == 0 + echomsg "Error: Content length is zero" + return + endif + + # Header and contents are separated by '\r\n\r\n' + idx = stridx(lsp_servers[ftype].data, "\r\n\r\n") + if idx == -1 + echomsg "Error: Content separator is not found" + return + endif + + idx = idx + 4 + + if lsp_servers[ftype].data->len() - idx < len + echomsg "Error: Didn't receive the complete message" + return + endif + + var content = lsp_servers[ftype].data[idx : idx + len - 1] + var reply = content->json_decode() + + if reply->has_key('id') + var req = lsp_servers[ftype].requests->get(string(reply.id)) + # Remove the corresponding stored request message + lsp_servers[ftype].requests->remove(string(reply.id)) + + if reply->has_key('error') + echomsg "Error: request " .. req.method .. " failed (" .. reply.error.message .. ")" + else + lsp#process_reply(req, reply) + endif + endif + + lsp_servers[ftype].data = lsp_servers[ftype].data[idx + len :] + endwhile +enddef + +def lsp#output_cb(ftype: string, chan: channel, msg: string): void + #writefile(split(msg, "\n"), 'lsp_server.out', 'a') + lsp_servers[ftype].data = lsp_servers[ftype].data .. msg + lsp#process_server_msg(ftype) +enddef + +def lsp#error_cb(ftype: string, chan: channel, emsg: string,): void + #writefile(split(emsg, "\n"), 'lsp_server.err', 'a') +enddef + +def lsp#exit_cb(ftype: string, job: job, status: number): void + echomsg "LSP server exited with status " .. status +enddef + +# Return the next id for a LSP server request message +def lsp#next_reqid(ftype: string): number + var id = lsp_servers[ftype].nextID + lsp_servers[ftype].nextID = id + 1 + return id +enddef + +# Send a request message to LSP server +def lsp#sendto_server(ftype: string, content: dict): void + var req_js: string = content->json_encode() + var msg = "Content-Length: " .. req_js->len() .. "\r\n\r\n" + var ch = lsp_servers[ftype].job->job_getchannel() + ch->ch_sendraw(msg) + ch->ch_sendraw(req_js) +enddef + +def lsp#create_reqmsg(ftype: string, method: string): dict + var req = {} + req.jsonrpc = '2.0' + req.id = lsp#next_reqid(ftype) + req.method = method + req.params = {} + + # Save the request, so that the corresponding response can be processed + lsp_servers[ftype].requests->extend({[string(req.id)]: req}) + + return req +enddef + +def lsp#create_notifmsg(ftype: string, notif: string): dict + var req = {} + req.jsonrpc = '2.0' + req.method = notif + req.params = {} + + return req +enddef + +# Send a "initialize" LSP request +def lsp#init_server(ftype: string): number + var req = lsp#create_reqmsg(ftype, 'initialize') + lsp#sendto_server(ftype, req) + return 1 +enddef + +# Start a LSP server +def lsp#start_server(ftype: string): number + if lsp_servers[ftype].running + echomsg "LSP server for " .. ftype .. " is already running" + return 0 + endif + + var cmd = [lsp_servers[ftype].path] + cmd->extend(lsp_servers[ftype].args) + + var opts = {'in_mode': 'raw', + 'out_mode': 'raw', + 'err_mode': 'raw', + 'out_cb': function('lsp#output_cb', [ftype]), + 'err_cb': function('lsp#error_cb', [ftype]), + 'exit_cb': function('lsp#exit_cb', [ftype])} + + #writefile([], 'lsp_server.out') + #writefile([], 'lsp_server.err') + lsp_servers[ftype].data = '' + lsp_servers[ftype].nextID = 1 + lsp_servers[ftype].requests = {} + + var job = job_start(cmd, opts) + if job->job_status() == 'fail' + echomsg "Error: Failed to start LSP server " .. lsp_servers[ftype].path + return 1 + endif + + lsp_servers[ftype].job = job + lsp_servers[ftype].running = v:true + + lsp#init_server(ftype) + + return 0 +enddef + +# Send a 'shutdown' request to the LSP server +def lsp#shutdown_server(ftype: string): void + var req = lsp#create_reqmsg(ftype, 'shutdown') + lsp#sendto_server(ftype, req) +enddef + +# Send a 'exit' notification to the LSP server +def lsp#exit_server(ftype: string): void + var req = lsp#create_notifmsg(ftype, 'exit') + lsp#sendto_server(ftype, req) +enddef + +# Stop a LSP server +def lsp#stop_server(ftype: string): number + if !lsp_servers[ftype].running + echomsg "LSP server for " .. ftype .. " is not running" + return 0 + endif + + lsp#shutdown_server(ftype) + + # Wait for the server to process the shutodwn request + sleep 1 + + lsp#exit_server(ftype) + + lsp_servers[ftype].job->job_stop() + lsp_servers[ftype].job = v:none + lsp_servers[ftype].running = v:false + lsp_servers[ftype].requests = {} + return 0 +enddef + +# Send a LSP "textDocument/didOpen" notification +def lsp#textdoc_didopen(fname: string, ftype: string): void + var notif = lsp#create_notifmsg(ftype, 'textDocument/didOpen') + + # interface DidOpenTextDocumentParams + # interface TextDocumentItem + var tdi = {} + tdi.uri = 'file://' .. fname + tdi.languageId = ftype + tdi.version = 1 + tdi.text = readfile(fname)->join("\n") + notif.params->extend({'textDocument': tdi}) + + lsp#sendto_server(ftype, notif) +enddef + +# Send a LSP "textDocument/didClose" notification +def lsp#textdoc_didclose(fname: string, ftype: string): void + var notif = lsp#create_notifmsg(ftype, 'textDocument/didClose') + + # interface DidCloseTextDocumentParams + # interface TextDocumentIdentifier + var tdid = {} + tdid.uri = 'file://' .. fname + notif.params->extend({'textDocument': tdid}) + + lsp#sendto_server(ftype, notif) +enddef + +# Goto a definition using "textDocument/definition" LSP request +def lsp#goto_definition(fname: string, ftype: string, lnum: number, col: number) + if fname == '' || ftype == '' + return + endif + if !lsp_servers->has_key(ftype) + echomsg 'Error: LSP server for "' .. ftype .. '" filetype is not found' + return + endif + if !lsp_servers[ftype].running + echomsg 'Error: LSP server for "' .. ftype .. '" filetype is not running' + return + endif + + var req = lsp#create_reqmsg(ftype, 'textDocument/definition') + + # interface DefinitionParams + # interface TextDocumentPositionParams + # interface TextDocumentIdentifier + req.params->extend({'textDocument': {'uri': 'file://' .. fname}}) + # interface Position + req.params->extend({'position': {'line': lnum, 'character': col}}) + + lsp#sendto_server(ftype, req) +enddef + +# Goto a declaration using "textDocument/declaration" LSP request +def lsp#goto_declaration(fname: string, ftype: string, lnum: number, col: number) + if fname == '' || ftype == '' + return + endif + if !lsp_servers->has_key(ftype) + echomsg 'Error: LSP server for "' .. ftype .. '" filetype is not found' + return + endif + if !lsp_servers[ftype].running + echomsg 'Error: LSP server for "' .. ftype .. '" filetype is not running' + return + endif + + var req = lsp#create_reqmsg(ftype, 'textDocument/declaration') + + # interface DeclarationParams + # interface TextDocumentPositionParams + # interface TextDocumentIdentifier + req.params->extend({'textDocument': {'uri': 'file://' .. fname}}) + # interface Position + req.params->extend({'position': {'line': lnum, 'character': col}}) + + lsp#sendto_server(ftype, req) +enddef + +def lsp#add_file(fname: string, ftype: string): void + if fname == '' || ftype == '' || !lsp_servers->has_key(ftype) + return + endif + if !lsp_servers[ftype].running + lsp#start_server(ftype) + endif + lsp#textdoc_didopen(fname, ftype) +enddef + +def lsp#remove_file(fname: string, ftype: string): void + if fname == '' || ftype == '' || !lsp_servers->has_key(ftype) + return + endif + if !lsp_servers[ftype].running + lsp#start_server(ftype) + endif + lsp#textdoc_didclose(fname, ftype) +enddef + +def lsp#stop_all_servers() + for [ftype, server] in items(lsp_servers) + if server.running + lsp#stop_server(ftype) + endif + endfor +enddef + +def lsp#add_server(ftype: string, serverpath: string, args: list) + var sinfo = { + 'path': serverpath, + 'args': args, + 'running': v:false, + 'job': v:none, + 'data': '', + 'nextID': 1, + 'requests': {} # outstanding LSP requests + } + lsp_servers->extend({[ftype]: sinfo}) +enddef + +def lsp#show_servers() + echomsg lsp_servers +enddef + +# vim: shiftwidth=2 sts=2 expandtab diff --git a/doc/lsp.txt b/doc/lsp.txt new file mode 100644 index 0000000..e69de29 diff --git a/plugin/lsp.vim b/plugin/lsp.vim new file mode 100644 index 0000000..442a724 --- /dev/null +++ b/plugin/lsp.vim @@ -0,0 +1,14 @@ +vim9script + +# LSP plugin for vim9 + +autocmd BufRead * call lsp#add_file(expand(':p'), &filetype) +autocmd BufWipeOut * call lsp#remove_file(expand(':p'), &filetype) + +# TODO: Is it needed to shutdown all the LSP servers when exiting Vim? +# This takes some time. +#autocmd VimLeavePre * call lsp#stop_all_servers() + +command! -nargs=0 LspGotoDefinition call lsp#goto_definition(expand('%:p'), &filetype, line('.') - 1, col('.') - 1) +command! -nargs=0 LspGotoDeclaration call lsp#goto_declaration(expand('%:p'), &filetype, line('.') - 1, col('.') - 1) + -- 2.48.1