October 16, 2019

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:

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.

:bulb: 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.

:gift: 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:

The LSC Plugin

After much trialling I chose LSC due to the following characteristics of the plugin:

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.

:nut_and_bolt: 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.

:exclamation: Whilst debugging a recalcitrant language server please do enable LSC diagnostics.

Screenshot

The LSC plugin auto-completing JavaScript.

vim_lsc_completion

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.

:gem: 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.

:steam_locomotive: 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.

:construction: 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.

:clap: 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

:warning: 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.