LSP in Vim with the LSC Plugin
The emergence of the Language Server Protocol (LSP) and asynchronous job support has given rise to a myriad of code completion frameworks for the Vim and Neovim editors.
So many choices is both a benefit and curse; the benefit being that the savvy Vim user can craft a configuration specific to their need, the curse, especially for a novice, is the classic paradox of choice, where to start and what to choose?
This post will discuss my code completion setup, and more broadly my LSP setup, for Ruby and JavaScript filetypes using the LSC plugin.
Note, my choices may not necessarily suit you, but they do offer a starting point for users wishing to use LSP-based code completion, and other advanced language-aware actions, in Vim.
Feel free to refer to my dotfiles to view my particular LSP configuration.
UPDATE (Sep 2021): I now use Neovim’s inbuilt LSP client instead of the LSC plugin. However, the core concepts discussed in this apply apply to whichever LSP client one chooses to use.
Prerequisites
A modern version of Vim or Neovim that supports asynchronous job control is required. That means Vim 8 or any modern version of Neovim.
Preferably, a very recent version of Vim, version 8.1.2050 or Neovim 0.4.0 at the time of this writing, is strongly recommended since the LSP hover operation of the LSC plugin can use either Vim’s Popup Window or Neovim’s Floating Window functionality if available.
I also recommend installing and updating Vim, or Neovim, using Homebrew on macOS and Linux.
For example, to install Neovim using Brew:
brew install neovim
And to update Neovim:
brew upgrade neovim
Language Server Protocol (LSP)
Created by Microsoft, LSP was originally developed for the Visual Studio Code editor to decouple code editing and presentation from language-specific actions.
Language-specific actions, such as: auto-completion, hovering and navigation, that used to be the purview of heavyweight IDEs are now available to LSP-capable editors when associated with an appropriate LSP-compliant language server. LSP transfers the responsibility of such language-specific actions out of the editor to a vendor-agnostic language server that runs as a separate background process on the host.
As an open JSON-RPC-based standard, LSP now has multi-vendor support which has led to the development of numerous language clients and servers.
Vim Omni Completion
The 2006 release of Vim 7 saw the introduction a new form of completion,
omni-completion. Omni-completion, orthogonal to existing forms of completion
such as keyword
or dictionary
completion, is performed by the defined
omnifunc
and will usually offer filetype-specific completions.
Invoked by <Control-x><Control-o>
, the intelligence of omni-completion depends
on the sophistication of the omnifunc
in use. Vim ships with set of
rudimentary omni completion implementations. Users can install more advanced
omni completion plugins, such as: Tern
for JavaScript or clang_complete
for C or C++, to improve the quality of such completions.
Nonetheless, there are a few issues with omni-completion:
-
completion is a synchronous operation, invoking omni-completion will block the editor until the operation is concluded
-
omni-completion plugins need to be coded and maintained specifically for Vim
However, with LSP-based completion, Vim can leverage and use the same language servers used by Visual Studio Code. One can be confident that the major language servers are actively developed and maintained. On the other hand, some omni-completion plugins, such as Tern for Vim, are no longer maintained.
Also, LSP-based solutions can leverage Vim & Neovim’s asynchronous job control
to not block the editor whilst editing. Auto-completion, where completion
candidates are displayed as one types, should be asynchronous otherwise
editing may be painful due to stalls arising from the synchronous invocation of
the omnifunc
.
LSP offers more than just code completion; a full-featured language server can provide context-aware navigation, code refactoring and hovering tooltips, among other capabilities.
My LSP configuration, documented below, readily allows for both non-blocking auto-completion or manually invoked omni-completion using the same language servers in both cases. You choose which type of completion suits.
Vim Completion Frameworks and LSP-clients
A Vim completion framework is responsible for collating completion candidates and displaying those choices to the user. Advanced completion frameworks often operate, by default, in asynchronous auto-completion mode.
An LSP client on the other-hand is editor tooling that supports communication with a language server employing the Language Server Protocol. As of the time of this post, October 2019, neither Vim nor Neovim provide out-of-the-box support for LSP. However, a future version of Neovim will provide LSP support as noted in this pull request.
UPDATE (Sep 2021): Neovim 0.5 ships with an LSP client.
In the meantime there are multiple Vim plugins that do provide LSP support.
Certain code completions frameworks also include direct LSP support whilst others delegate such duties to a separate LSP-client plugin.
Notable code completion and LSP-client plugins for Vim and Neovim:
-
YouCompleteMe, a monolithic code completion engine that predates LSP and asynchronous job control in Vim, both of which have now been incorporated. For many years this was the go to code completion plugin for Vim, nowadays there are lighter weight alternatives.
-
deoplete by Shogo, the first asynchronous code completion framework for Neovim, but which now also supports Vim. Completion candidates are gathered from deoplete-compatible sources; note, LSP-based results require integration with the LanguageClient-neovim plugin.
-
ncm2 by roxma, is another asynchronous code completion framework for Vim and Neovim. Conceptually similar to deoplete, completion candidates are gathered from ncm2-compatible sources; however, LSP-based candidates require integration with either the LanguageClient-neovim or ncm2-vim-lsp plugins.
-
Coc, a contemporary code completion framework for Neovim and Vim with inbuilt LSP support. Being Typescript-based allows Coc to leverage existing plugins used by Visual Studio Code. Somewhat against the norm, Coc operates its own configuration and extension system.
-
Completor, an asynchronous completion framework for Vim, and now also Neovim-compatible, with inbuilt LSP support. Leaner and more accessible than the plugins mentioned above.
-
LanguageClient-neovim, an LSP client commonly used in combination with an asynchronous completion framework such as deoplete or ncm2. The name implies Neovim-only support, but nowadays it also supports Vim.
-
vim-lsp, an LSP client written in Vimscript; unlike some Python-based clients listed above. This plugin is frequently used with the asyncomplete.vim plugin by the same author.
-
asyncomplete.vim, an ayschronous auto-completion framework written in Vimscript that supports both Vim and Neovim’s asynchronous job control APIs. Like deoplete and ncm2, LSP-based candidates require integration with an LSP-client plugin, this time with the vim-lsp plugin.
-
ALE, primarily an asynchronous linting and fixing plugin, but now extended to support LSP. Language servers can provide linting, hence the reason why ALE integrated LSP. ALE now includes LSP-based code completion in addition to other LSP functionality.
-
LSC by Nate Bosch, a performant LSP client, written in Vimscript, that supports LSP-based asynchronous auto-completion in both Vim and Neovim.
-
MUcomplete, a minimalist auto-completion plugin that leverages Vim’s existing completion infrastructure. This plugin does not support asynchronous operation.
-
Supertab, a tab completion plugin for Vim. This plugin simply maps the TAB key to Vim’s existing completion kinds.
-
VimCompletesMe, another tab completion plugin for Vim, similar to Supertab but simpler.
The LSC Plugin
After much trialling I chose LSC due to the following characteristics of the plugin:
-
LSC is a combination LSP-client and completion plugin that stands alone, there is no multi-plugin dance required
-
Implemented in pure Vimscript, this eases installation and avoids certain kinds of upgrade pain that may be experienced by Python-based alternatives
-
Compatible with both Vim and Neovim’s differing asynchronous job control APIs
-
Light in weight, less than three thousand lines of Vimscript; LSC augments Vim, it does not take over unlike certain other plugins
-
Excellent performance due to: asynchronous operation and use of performance optimizations such as incremental updates and debouncing
-
Pristine completions, candidates will only be sourced from the language server, there will be no mixing of candidates from other completion sources
-
Straightforward and concise
~/.vimrc
-based configuration -
Referenced language servers are installed and maintained externally, similar to how the ALE plugin uses external linters and fixers
-
Simple configuration option to select either asynchronous auto-completion or synchronous manual completion, once the
omnifunc
option is appropriately set -
Auto-completion, if enabled, will only apply to filetypes that are associated with a language server
-
The
find all references
operation will populate the quickfix list; no custom UIs in contrast to certain other LSP plugins -
The
symbol hover
operation, often mapped toK
, can use either Vim’s popup window or Neovim’s floating window APIs if available, whilst doubleK
will convert a popup/floating window into split-preview window if hover persistence is desired -
Easy to use
code actions
operation, just select the listed number to carry out a specific language/framework action, for example wrapping a widget with another widget in Flutter
The simplicity LSC may not suit you, especially if you wish to combine
completion candidates from multiple sources, say LSP-based candidates with
keyword-in-file candidates. In my case I already have ctrl
-based mappings
that allow easy switching from one completion kind to
another.
I augment LSC with the VimCompletesMe plugin since I like using the TAB key to scroll through completion candidates, among other benefits of that plugin.
LSC Installation & Configuration
If using the vim-plug plugin manager,
add the following to your ~/.vimrc
file:
Plug 'natebosch/vim-lsc'
Plug 'ajh17/VimCompletesMe'
Then run :PlugInstall
to install the plugins. Please change the notation
appropriately if using an alternate plugin manager for Vim.
My Ruby and JavaScript LSC configuration:
let g:lsc_server_commands = {
\ 'ruby': {
\ 'command': 'solargraph stdio',
\ 'log_level': -1,
\ 'suppress_stderr': v:true,
\ },
\ 'javascript': {
\ 'command': 'typescript-language-server --stdio',
\ 'log_level': -1,
\ 'suppress_stderr': v:true,
\ }
\}
let g:lsc_auto_map = {
\ 'GoToDefinition': 'gd',
\ 'FindReferences': 'gr',
\ 'Rename': 'gR',
\ 'ShowHover': 'K',
\ 'FindCodeActions': 'ga',
\ 'Completion': 'omnifunc',
\}
let g:lsc_enable_autocomplete = v:true
let g:lsc_enable_diagnostics = v:false
let g:lsc_reference_highlights = v:false
let g:lsc_trace_level = 'off'
For a given filetype the LSC plugin will take care of launching, communicating
and shutting down the named language server command
. The specified language
servers, listed above, will be discussed in greater detail in the following Ruby
and JavaScript sections.
In addition to LSP-based auto-completion, I define and use the following four LSP actions:
LSP Action | LSC Command | Vim Mapping |
---|---|---|
Go to definition for the symbol under the cursor | GoToDefinition |
gd |
Find all references for the symbol under the cursor | FindReferences |
gr |
Rename the symbol under the cursor and all references | Rename |
gR |
Show hover tooltip for the symbol under the cursor | ShowHover |
K |
Find code actions at the cursor location | FindCodeActions |
ga |
I strongly recommend the following completeopt
setting when using
auto-completion:
set completeopt=menu,menuone,noinsert,noselect
If you do not care for auto-completion but do wish to use LSP-based
omni-completion, via <Control-x><Control-o>
, then add the following to your
~/.vimrc
:
let g:lsc_enable_autocomplete = v:false
I use the ALE plugin for linting and
fixing, specifically StandardRB for
Ruby and StandardJS for JavaScript, hence, I disable
LSC diagnostics. However, if you wish to use LSP-based real-time linting, and
your language server supports it, then specify let g:lsc_enable_diagnostics =
v:true
.
Lastly, I configure LSC to suppress all client/server messages; by default the LSC plugin is a little too chatty with regard to displaying all messages, even when they are not that useful.
Whilst debugging a recalcitrant language server please do enable LSC diagnostics.
Screenshot
The LSC plugin auto-completing JavaScript.
Ruby Language Server
Solargraph is an LSP-compliant language server for Ruby.
Install Solargraph with the following command:
gem install solargraph
For LSC to function please ensure solargraph
is available in your $PATH
.
The intelligence of Solargraph, when operating in a Gemfile
-managed
projects, can be improved by running following command in the project’s base
directory:
solargraph bundle
Solargraph will now use the documentation from the project’s Gems for improved completions.
Similarly, the quality of Solargraph completions will be
further enhanced for Rails
projects by also copying this
Gist into
your project, I copied that file into the initializers
directory.
Lastly, Solargraph is a still maturing technology, so please install updates when they become available.
JavaScript Language Server
Sourcegraph’s language
server,
javascript-typescript-langserver
, is often noted as the language server to use
for JavaScript and TypeScript. However, I have found it to be a little slow and
somewhat buggy; for example the find all references action regularly failed
with my React projects.
Interestingly, Visual Studio Code actually uses editor-provided TypeScript
technologies, instead of Sourcegraph’s language server, for JavaScript and
TypeScript language actions. Unfortunately, Microsoft’s stand-alone TypeScript
server, tsserver
, is not LSP-compliant, but it does provide the core
capabilities needed by an LSP client.
Thankfully TypeFox does provide a
shim between tsserver
and
the Language Server Protocol with the TypeScript Language
Server package. This
is the language server I recommend for JavaScript and TypeScript filetypes.
Install the TypeScript Language Server with the following command:
npm install -g typescript-language-server
For LSC to function please ensure typescript-language-server
is available in
your $PATH
.
Language Servers
Available language servers for certain prevalent programming languages:
Language | Language Server | Command |
---|---|---|
C/C++ | clangd | clangd |
Go | gopls | gopls serve |
Python | pyright | pyright --stdio |
Rust | Rust Analyzer | rust-analyzer |
Swift | SourceKit-LSP | sourcekit-lsp |
Note, I have not tested these language servers personally.
Conclusion
Which plugin(s) one ultimately uses is not that interesting, what is genuinely game-changing are the advanced editing capabilities that LSP provides.
This Language Server Protocol Vim screencast, by Greg Hurrell, is pertinent with respect to that point:
Hopefully this post provides enough detail to start your LSP journey in Vim.