├── after └── plugin │ └── lspcommand.vim ├── plugin └── lspcommand.vim ├── README.md ├── doc └── lspcommand.txt └── lua └── lspcommand.lua /after/plugin/lspcommand.vim: -------------------------------------------------------------------------------- 1 | if !get(g:, 'lsp_legacy_commands', v:false) 2 | silent! delcommand LspInfo 3 | silent! delcommand LspStart 4 | silent! delcommand LspStop 5 | silent! delcommand LspRestart 6 | endif 7 | -------------------------------------------------------------------------------- /plugin/lspcommand.vim: -------------------------------------------------------------------------------- 1 | function! s:comp(ArgLead, CmdLine, CursorPos) 2 | return luaeval('require"lspcommand".comp(_A[1], _A[2], _A[3])', [ 3 | \ a:ArgLead, a:CmdLine, a:CursorPos, 4 | \ ]) 5 | endfunction 6 | 7 | command! -bar -range=% -nargs=+ -complete=customlist,s:comp Lsp 8 | \ call luaeval('require"lspcommand".run(_A)', { 9 | \ 'args': , 'range': , 'line1': , 'line2': , 10 | \ }) 11 | 12 | if !get(g:, 'lsp_no_lowercase', v:false) 13 | cnoreabbrev lsp getcmdtype() ==# ':' && 14 | \ (getcmdline() ==# 'lsp' getcmdline() ==# "'<,'>lsp") ? 'Lsp' : 'lsp' 15 | endif 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lsp-command 2 | 3 | Command interface for neovim LSP. 4 | 5 | Provides full access to LSP features with a single `:Lsp` command. To give few examples, 6 | instead of `:lua vim.lsp.buf.workspace_symbol('foo')`, you can simply write `:Lsp find foo`. 7 | To format a range of lines, make a visual selection and write `:Lsp format` (or 8 | abbreviate it to just `:Lsp f`). `lspconfig` commands are now subcommands for `:Lsp`: 9 | `:LspInfo` is now `:Lsp info`/`:Lsp?`, `:LspStart` is `:Lsp start` etc. 10 | 11 | Completion for `:Lsp` command is contextual. Only completions valid in the current 12 | context are suggested. That is, if the current buffer is not attached to any server, the 13 | only completions will be `info`, `start`, `stop`, `restart`. If buffer is attached, only 14 | actions supported by the attached language server are suggested. 15 | 16 | Custom commands in vim have to start with an uppercase letter, but this plugin goes around 17 | it by defining a basic command line abbreviation, or an alias, from the lowercase `:lsp`. 18 | This means all of the examples above can be also written without capitalizing the first 19 | letter: `:lsp find foo`, `:lsp format`, `:lsp info` etc. 20 | 21 | The interface is not final. Command names, their arguments and how they can be abbreviated 22 | can change at any time. 23 | 24 | Requires `neovim/lspconfig`. 25 | 26 | ## Usage 27 | 28 | [`:h :Lsp`](doc/lspcommand.txt) 29 | -------------------------------------------------------------------------------- /doc/lspcommand.txt: -------------------------------------------------------------------------------- 1 | *lspcommand.txt* Lsp command 2 | 3 | ============================================================================== 4 | :Lsp {subcommand} [arguments] *:Lsp* *:lsp* *lsp-command* 5 | Lowercase :lsp alias is defined by default, meaning you don't 6 | have to capitalize the command name. Example: `:lsp def` 7 | 8 | ------------------------------------------------------------------------------ 9 | GLOBAL COMMANDS *lsp-command-global* 10 | 11 | :Lsp in[fo] *:Lsp-info* *:Lsp?* 12 | Shows the status of active and configured language servers. 13 | 14 | :Lsp st[art] [{server-name}] *:Lsp-start* 15 | Start the requested server name. 16 | Will only successfully start if the command detects a root directory 17 | matching the current config. Pass `autostart = false` to your `.setup{}` 18 | call for a language server if you would like to launch clients solely 19 | with this command. Defaults to all servers matching current buffer 20 | filetype. 21 | 22 | Example: `:Lsp start clangd` 23 | 24 | :Lsp sto[p] [{client-ids}] *:Lsp-stop* 25 | Manually stops the given language client(s). 26 | Defaults to stopping all buffer clients. 27 | 28 | :Lsp res[tart] [{client-ids}] *:Lsp-restart* 29 | Manually restart the given language client(s). 30 | Defaults to restarting all buffer clients. 31 | 32 | ------------------------------------------------------------------------------ 33 | BUFFER COMMANDS *lsp-command-buffer* 34 | 35 | :Lsp d[efinition] *:Lsp-definition* 36 | |vim.lsp.buf.definition()| 37 | 38 | :Lsp dec[laration] *:Lsp-declaration* 39 | |vim.lsp.buf.declaration()| 40 | 41 | :Lsp i[mplementation] *:Lsp-implementation* 42 | |vim.lsp.buf.implementation()| 43 | 44 | :Lsp t[ypedefinition] *:Lsp-typedefinition* 45 | |vim.lsp.buf.type_definition()| 46 | 47 | :Lsp r[eferences] *:Lsp-references* 48 | |vim.lsp.buf.references()| 49 | 50 | :Lsp h[over] *:Lsp-hover* 51 | |vim.lsp.buf.hover()| 52 | 53 | :Lsp si[gnature] *:Lsp-signature* 54 | |vim.lsp.buf.signature_help()| 55 | 56 | :Lsp ren[ame] [{new_name}] *:Lsp-rename* 57 | Renames the symbol under the cursor to {new_name}. If new name is 58 | not provided, prompts for a new name. 59 | 60 | |vim.lsp.buf.rename()| 61 | 62 | :[range]Lsp f[ormat] [sync[={timeout}]] [order={order}] *:Lsp-format* 63 | Formats the current buffer. 64 | 65 | If sync is given, performs formatting synchronously, with optional 66 | {timeout} in milliseconds. Useful for running on save, to make sure 67 | buffer is formatted prior to being saved. 68 | 69 | When multiple clients are attached, the {order} of semicolon delimited 70 | client names can be specified. Formatting is requested from clients in 71 | the following order: first all clients that are not in the {order} 72 | list, then the remaining clients in the order as they occur in the 73 | {order} list. {order} implies {sync}. 74 | 75 | Example: `:Lsp format sync=500 order=clangd,gopls,tsserver` 76 | 77 | |vim.lsp.buf.formatting()|, |vim.lsp.buf.range_formatting()|, 78 | |vim.lsp.buf.formatting_sync()|, |vim.lsp.buf.formatting_seq_sync()| 79 | 80 | :[range]Lsp c[odeaction] [{kind}] *:Lsp-codeaction* 81 | Selects a code action available at the current cursor position, or 82 | for a given range. 83 | 84 | Optional {kind} will filter the code actions, if supported by the 85 | language server. 86 | 87 | |vim.lsp.buf.code_action()|, |vim.lsp.buf.range_code_action()| 88 | 89 | :Lsp s[ymbols] *:Lsp-symbols* 90 | |vim.lsp.buf.document_symbol()| 91 | 92 | :Lsp fi[nd] [{name}] *:Lsp-find* 93 | Lists symbols in the current workspace matching {name}. If no name is 94 | provided, lists all symbols. 95 | 96 | |vim.lsp.buf.workspace_symbol()| 97 | 98 | :Lsp w[orkspace] [add,remove {folder}] *:Lsp-workspace* 99 | Without arguments lists workspace folders. 100 | 101 | Example: `Lsp workspace add path/to/dir` 102 | 103 | |vim.lsp.buf.list_workspace_folders()|, 104 | |vim.lsp.buf.add_workspace_folder()|, 105 | |vim.lsp.buf.remove_workspace_folder()| 106 | 107 | :Lsp inc[omingcalls] *:Lsp-incomingcalls* 108 | |vim.lsp.buf.incoming_calls()| 109 | 110 | :Lsp o[utgoingcalls] *:Lsp-outgoingcalls* 111 | |vim.lsp.buf.outgoing_calls()| 112 | 113 | ------------------------------------------------------------------------------ 114 | OPTIONS *lsp-command-options* 115 | 116 | *g:lsp_no_lowercase* 117 | Set to true to disable the lowercase :lsp command alias. 118 | 119 | *g:lsp_legacy_commands* 120 | Set to true to enable legacy commands: :LspInfo, :LspStart, etc. 121 | 122 | ============================================================================== 123 | vim:tw=78:sw=8:sts=8:et:ft=help:norl: 124 | -------------------------------------------------------------------------------- /lua/lspcommand.lua: -------------------------------------------------------------------------------- 1 | local api, fn = vim.api, vim.fn 2 | 3 | local function echo(msg) 4 | api.nvim_echo({{msg}}, false, {}) 5 | return nil 6 | end 7 | 8 | local function echoerr(msg) 9 | api.nvim_echo({{'Lsp: '..msg, 'ErrorMsg'}}, true, {}) 10 | return nil 11 | end 12 | 13 | local function get_clients() 14 | if vim.lsp.get_active_clients then 15 | return vim.lsp.get_active_clients({ bufnr = 0 }) 16 | else 17 | return vim.lsp.buf_get_clients(0) 18 | end 19 | end 20 | 21 | local function available_servers() 22 | local lspconfig = require('lspconfig') 23 | if lspconfig.util.available_servers then 24 | return lspconfig.util.available_servers() 25 | else 26 | return lspconfig.available_servers() 27 | end 28 | end 29 | 30 | local function complete_filter(arglead, candidates) 31 | if not candidates then 32 | return {} 33 | end 34 | arglead = arglead or '' 35 | 36 | local results = {} 37 | for _, k in ipairs(candidates) do 38 | if fn.stridx(k, arglead) == 0 then 39 | table.insert(results, k) 40 | end 41 | end 42 | return results 43 | end 44 | 45 | local function complete_active_clients(args) 46 | return complete_filter(args[#args], vim.tbl_map(function(client) 47 | return ("%d (%s)"):format(client.id, client.name) 48 | end, vim.lsp.get_active_clients())) 49 | end 50 | 51 | local function parse_clients(args) 52 | local clients = {} 53 | for _, arg in ipairs(args) do 54 | if arg:match('^%d+$') then 55 | local client = vim.lsp.get_client_by_id(tonumber(arg)) 56 | if client == nil then 57 | return echoerr('No client with id '..arg) 58 | end 59 | clients[arg] = client 60 | elseif not arg:match('^%([%w_%-]+%)$') then 61 | return echoerr('Invalid argument: '..arg) 62 | end 63 | end 64 | 65 | if vim.tbl_isempty(clients) then 66 | return vim.lsp.get_active_clients() 67 | end 68 | return vim.tbl_values(clients) 69 | end 70 | 71 | local function simple_command(name, func, capability) 72 | return { 73 | command = name, 74 | attached = true, 75 | range = false, 76 | run = function(args) 77 | if #args ~= 0 then 78 | return echoerr('No arguments allowed') 79 | end 80 | vim.lsp.buf[func]() 81 | end, 82 | capability = capability, 83 | } 84 | end 85 | 86 | local definition = simple_command('definition', 'definition', 'definitionProvider') 87 | local declaration = simple_command('declaration', 'declaration', 'declarationProvider') 88 | local typedefinition = simple_command('typedefinition', 'type_definition', 'typeDefinitionProvider') 89 | local symbols = simple_command('symbols', 'document_symbol', 'documentSymbolProvider') 90 | local hover = simple_command('hover', 'hover', 'hoverProvider') 91 | local implementation = simple_command('implementation', 'implementation', 'implementationProvider') 92 | local references = simple_command('references', 'references', 'referencesProvider') 93 | local signature = simple_command('signature', 'signature_help', 'signatureHelpProvider') 94 | local incomingcalls = simple_command('incomingcalls', 'incoming_calls', 'callHierarchyProvider') 95 | local outgoingcalls = simple_command('outgoingcalls', 'outgoing_calls', 'callHierarchyProvider') 96 | 97 | local rename = { 98 | command = 'rename', 99 | attached = true, 100 | range = false, 101 | run = function(args) 102 | if #args > 1 then 103 | return echoerr('Expected zero or one argument') 104 | end 105 | vim.lsp.buf.rename(args[1]) 106 | end, 107 | capability = 'renameProvider', 108 | } 109 | 110 | local find = { 111 | -- TODO: different name? "symbols" is used by document_symbol 112 | command = 'find', 113 | attached = true, 114 | range = false, 115 | run = function(args) 116 | if #args > 1 then 117 | return echoerr('Expected zero or one argument') 118 | end 119 | vim.lsp.buf.workspace_symbol(args[1] or '') 120 | end, 121 | capability = 'workspaceSymbolProvider', 122 | } 123 | 124 | local codeaction = { 125 | command = 'codeaction', 126 | attached = true, 127 | -- TODO: would be cool to provide a /pattern/ to filter code actions 128 | run = function(args, range) 129 | if #args > 1 then 130 | return echoerr('Expected zero or one argument') 131 | end 132 | local context = { only = args[1] } 133 | if range then 134 | vim.lsp.buf.range_code_action(context, range[1], range[2]) 135 | else 136 | vim.lsp.buf.code_action(context) 137 | end 138 | end, 139 | complete = function(args) 140 | if #args == 1 then 141 | local kinds = {} 142 | for _, client in pairs(get_clients()) do 143 | local code_action = client.server_capabilities.codeActionProvider 144 | if type(code_action) == 'table' then 145 | for _, kind in ipairs(code_action.codeActionKinds) do 146 | kinds[kind] = true 147 | end 148 | end 149 | end 150 | return complete_filter(args[#args], vim.tbl_keys(kinds)) 151 | end 152 | end, 153 | capability = 'codeActionProvider', 154 | } 155 | 156 | local format = { 157 | command = 'format', 158 | attached = true, 159 | run = function(args, range) 160 | -- TODO: FormattingOptions 161 | local sync, order 162 | local function parse_arg(arg) 163 | if arg == 'sync' then 164 | sync = true 165 | return true 166 | end 167 | 168 | local match = arg:match('^sync=(%d+)$') 169 | if match then 170 | sync = tonumber(match) 171 | return true 172 | end 173 | 174 | match = arg:match('^order=([%w_%-,]+)$') 175 | if match then 176 | order = vim.split(match, ',', { trimempty = true, plain = true }) 177 | return true 178 | end 179 | end 180 | 181 | for _, arg in ipairs(args) do 182 | if not parse_arg(arg) then 183 | return echoerr('Invalid argument: '..arg) 184 | end 185 | end 186 | 187 | if order and not sync then 188 | sync = true 189 | end 190 | 191 | if sync then 192 | if range then 193 | return echoerr('Range not allowed for synchronous formatting') 194 | end 195 | 196 | if sync == true then 197 | sync = nil 198 | end 199 | 200 | if order then 201 | if vim.lsp.buf.format then 202 | for _, name in pairs(order) do 203 | vim.lsp.buf.format({ async = false, timeout_ms = sync, name = name }) 204 | end 205 | else 206 | vim.lsp.buf.formatting_seq_sync(nil, sync, order) 207 | end 208 | else 209 | if vim.lsp.buf.format then 210 | vim.lsp.buf.format({ async = false, timeout_ms = sync }) 211 | else 212 | vim.lsp.buf.formatting_sync(nil, sync) 213 | end 214 | end 215 | else 216 | if range then 217 | if vim.lsp.buf.format then 218 | vim.lsp.buf.format({ range = range }) 219 | else 220 | vim.lsp.buf.range_formatting(nil, range[1], range[2]) 221 | end 222 | else 223 | if vim.lsp.buf.format then 224 | vim.lsp.buf.format({ async = true }) 225 | else 226 | vim.lsp.buf.formatting() 227 | end 228 | end 229 | end 230 | end, 231 | complete = function(args) 232 | local arg = args[#args] 233 | local start, match = arg:match('^order=(.-)([^,]*)$') 234 | if start then 235 | local values = vim.split(start, ',', { trimempty = true, plain = true }) 236 | local lookup = {} 237 | for _, k in ipairs(values) do 238 | lookup[k] = true 239 | end 240 | local results = {} 241 | for _, server in ipairs(available_servers()) do 242 | if not lookup[server] and fn.stridx(server, match) == 0 then 243 | table.insert(results, 'order='..start..server) 244 | end 245 | end 246 | return results 247 | else 248 | return complete_filter(arg, {'sync', 'order='}) 249 | end 250 | end, 251 | -- TODO: check documentRangeFormattingProvider 252 | capability = 'documentFormattingProvider', 253 | } 254 | 255 | local workspace = { 256 | command = 'workspace', 257 | range = false, 258 | attached = true, 259 | run = function(args) 260 | if #args == 0 then 261 | local folders = vim.lsp.buf.list_workspace_folders() 262 | if #folders > 0 then 263 | for _, folder in ipairs(folders) do 264 | echo(folder) 265 | end 266 | else 267 | echo('No workspace folders') 268 | end 269 | elseif #args == 2 then 270 | if args[1] == 'add' then 271 | vim.lsp.buf.add_workspace_folder(fn.fnamemodify(args[2], ':p')) 272 | elseif args[1] == 'remove' then 273 | vim.lsp.buf.remove_workspace_folder(args[2]) 274 | else 275 | return echoerr('Invalid argument: '..args[1]) 276 | end 277 | else 278 | return echoerr('Invalid arguments') 279 | end 280 | end, 281 | complete = function(args) 282 | if #args == 1 then 283 | return complete_filter(args[#args], {'add', 'remove'}) 284 | elseif #args == 2 then 285 | if args[1] == 'add' then 286 | return fn.getcompletion(args[#args], 'dir') 287 | elseif args[1] == 'remove' then 288 | return complete_filter(args[#args], vim.lsp.buf.list_workspace_folders()) 289 | end 290 | end 291 | end, 292 | capability = 'workspaceFolders', 293 | } 294 | 295 | local info = { 296 | command = 'info', 297 | range = false, 298 | run = function(args) 299 | if #args ~= 0 then 300 | return echoerr('No arguments allowed') 301 | end 302 | require('lspconfig.ui.lspinfo')() 303 | end, 304 | } 305 | 306 | local function launch(config) 307 | if config.launch then 308 | config.launch() 309 | else 310 | config.autostart() 311 | end 312 | end 313 | 314 | local start = { 315 | command = 'start', 316 | range = false, 317 | run = function(args) 318 | if #args > 1 then 319 | return echoerr('Expected zero or one argument') 320 | end 321 | local server_name = args[1] 322 | local configs = require('lspconfig.configs') 323 | if server_name then 324 | if configs[server_name] then 325 | launch(configs[server_name]) 326 | else 327 | return echoerr('Server not found: '..server_name) 328 | end 329 | else 330 | local buffer_filetype = vim.bo.filetype 331 | for _, config in pairs(configs) do 332 | for _, filetype_match in ipairs(config.filetypes or {}) do 333 | if buffer_filetype == filetype_match then 334 | launch(config) 335 | end 336 | end 337 | end 338 | end 339 | end, 340 | complete = function(args) 341 | if #args == 1 then 342 | return complete_filter(args[#args], available_servers()) 343 | end 344 | end, 345 | } 346 | 347 | local stop = { 348 | command = 'stop', 349 | range = false, 350 | run = function(args) 351 | local clients = parse_clients(args) 352 | if clients == nil then return end 353 | for _, client in ipairs(clients) do 354 | client.stop() 355 | end 356 | end, 357 | complete = complete_active_clients, 358 | } 359 | 360 | local restart = { 361 | command = 'restart', 362 | range = false, 363 | run = function(args) 364 | local clients = parse_clients(args) 365 | if clients == nil then return end 366 | for _, client in ipairs(clients) do 367 | local configs = require('lspconfig.configs') 368 | client.stop() 369 | vim.defer_fn(function() 370 | launch(configs[client.name]) 371 | end, 500) 372 | end 373 | end, 374 | complete = complete_active_clients, 375 | } 376 | 377 | 378 | local commands = { 379 | codeaction, 380 | definition, 381 | declaration, 382 | typedefinition, 383 | symbols, 384 | hover, 385 | implementation, 386 | references, 387 | signature, 388 | rename, 389 | format, 390 | find, 391 | workspace, 392 | info, 393 | start, 394 | stop, 395 | restart, 396 | incomingcalls, 397 | outgoingcalls, 398 | } 399 | 400 | for _, command in ipairs(commands) do 401 | local name = command.command 402 | command.pattern = '\\V\\C\\^'..name:sub(1,1)..'\\%['..name:sub(2)..']\\$' 403 | end 404 | 405 | local function match_command(s) 406 | for _, command in ipairs(commands) do 407 | if fn.match(s, command.pattern) == 0 then 408 | return command 409 | end 410 | end 411 | end 412 | 413 | 414 | local function comp(ArgLead, CmdLine, CursorPos) 415 | local cmdline = fn.strpart(CmdLine, 0, CursorPos) -- trim cmdline to cursor position 416 | -- TODO: handle incomplete command name like ":Ls" 417 | -- also might not work with ":/Lsp /Lsp start" 418 | local begin = fn.match(cmdline, [[\v\C%(^|[^A-Za-z])\zsLsp%($|\s)]]) 419 | if begin < 0 then return {} end 420 | local args = fn.split(fn.strpart(cmdline, begin)) -- split arguments 421 | if #args < 1 then return {} end 422 | 423 | local has_range = fn.strpart(CmdLine, 0, begin):match('%S') ~= nil 424 | local is_attached = false 425 | local caps = {} 426 | for _, client in pairs(get_clients()) do 427 | is_attached = true 428 | for k, v in pairs(client.server_capabilities) do 429 | if v then 430 | caps[k] = true 431 | end 432 | end 433 | end 434 | 435 | -- remove first argument, "Lsp" 436 | table.remove(args, 1) 437 | -- add empty argument 438 | if CmdLine:sub(CursorPos, CursorPos):match('%s') then 439 | table.insert(args, '') 440 | end 441 | 442 | -- complete subcommand 443 | if #args < 2 then 444 | local results = {} 445 | for _, command in ipairs(commands) do 446 | if (command.range == nil or command.range == has_range) and 447 | (not command.attached or is_attached) and 448 | (not command.capability or caps[command.capability]) and 449 | fn.stridx(command.command, ArgLead) == 0 then 450 | table.insert(results, command.command) 451 | end 452 | end 453 | return results 454 | end 455 | 456 | -- complete for subcommand arguments 457 | local command = match_command(args[1]) 458 | if not command or 459 | not (command.range == nil or command.range == has_range) or 460 | not (not command.attached or is_attached) then 461 | return {} 462 | end 463 | 464 | local complete = command.complete 465 | if complete then 466 | table.remove(args, 1) 467 | return complete(args) 468 | end 469 | return {} 470 | end 471 | 472 | local function run(ctx) 473 | local args = fn.split(ctx.args) 474 | if #args == 0 then 475 | return echoerr('Expected subcommand') 476 | end 477 | 478 | -- :Lsp? 479 | if ctx.args:match('%s*%?%s*') then 480 | return require('lspconfig.ui.lspinfo')() 481 | end 482 | 483 | local command = match_command(args[1]) 484 | if command == nil then 485 | return echoerr('Invalid subcommand: '..args[1]) 486 | end 487 | table.remove(args, 1) 488 | 489 | local range 490 | if ctx.range == 1 then 491 | range = {{ ctx.line1, 0 }, { ctx.line1, 0 }} 492 | elseif ctx.range == 2 then 493 | range = {{ ctx.line1, 0 }, { ctx.line2, 0 }} 494 | end 495 | if command.range == false and range ~= nil then 496 | return echoerr('No range allowed for '..command.command..' subcommand') 497 | end 498 | 499 | if command.attached == true then 500 | if not (function() 501 | for _ in pairs(get_clients()) do 502 | return true 503 | end 504 | return false 505 | end)() then 506 | return echoerr('Buffer not attached') 507 | end 508 | end 509 | 510 | command.run(args, range) 511 | end 512 | 513 | return { 514 | run = run, 515 | comp = comp, 516 | } 517 | --------------------------------------------------------------------------------