Skip to main content

Command Palette

Search for a command to run...

Neovim with the TypeScript language server (LSP)

Like vim but shinier

Updated
5 min read
K

Site Reliability Engineer, currently at GitLab. All content and views are my own.

Software Developer by trade. Based in Auckland.

Cat person 😺 Father 👨‍👩‍👧, cyclist 🚲, Kiwi 🇳🇿.

I’m trying a few new things.

I’ve got a project that I’ve been experimenting with and I plan to publish as open source at some point. For this project I decided to revisit my old friend TypeScript. It’s a language I’ve used in the past, but I haven’t touched it professionally.

I’ve also been using Neovim more and more as my IDE instead of my old default Visual Studio Code. Especially for the smaller hobby projects. Switching it up a bit.

If you’re unfamiliar, definitely check it out, it is Vim but easier to customise and extend with Lua scripting. One big benefit is the variety of third party plugins you can find for it. Someone will burn me for this comparison, but Neovim reminds me of Emacs and its extensibility with Elisp, which is something I dabbled with earlier on in my career.

One thing that has been tricky though is that, while popular, Neovim is fairly new and at the time of writing is on v0.11.x. Some things are changing quickly and some things don’t have a lot of examples online.

Today I figured out TypeScript LSP and I thought I’d share it.

Dependencies

You may be running npm that pulls in some dependencies for you. For example I have versioned and local typescript per package.json that I pull in. But because nvim is global, you should set up some global dependencies.

You’ll need to install npm install -g typescript typescript-language-server

The former you already know, the latter comes from https://github.com/typescript-language-server/typescript-language-server and is a standalone binary.

If you haven’t already, installing nvim I will leave it as an exercise for the reader. I also alias vi to nvim because I have a few decades of habit of typing vi to open files.

init.lua

nvim is configured by init.lua, which is not there by default if you haven’t already started configuring it.

Make sure ~/.config/nvim/ exists, create it if not.

nvim ~/.config/nvim/init.lua

And this is where all the docs I’ve found get out of date. From nvim v0.11.x, the LSP config is:

" Enable TypeScript via the Language Server Protocol (LSP)
vim.lsp.enable('tsserver')

" Set the TS config for the LSP
vim.lsp.config('tsserver', {
  " Make sure this is on your path
  cmd = {'typescript-language-server', '--stdio'},
  filetypes = { 'typescript' },
  " This is a hint to tell nvim to find your project root from a file within the tree
  root_dir = vim.fs.root(0, {'package.json', '.git'}),
  on_attach = on_attach,
  capabilities = capabilities,
  " optional settings = {...} go here, refer to language server code: https://github.com/typescript-language-server/typescript-language-server/blob/5c483349b7b4b6f79d523f8f4d854cbc5cec7ecd/src/ts-protocol.ts#L379
})

Then you can set some key bindings, this is what I’m starting with, I’ll adapt and add some more later. Note that these are not specific to the TypeScript LSP, you can find more examples online for LSP keybindings:

-- lsp keymaps
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, {})
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, {})
vim.keymap.set('n', '<leader>f', function()
  vim.lsp.buf.format { async = true }
end, {})

The ’n’ above tells it to enable these keybindings in normal vim mode (not insert mode). And <leader> can be configured, defaults to \

Testing

Now open up a typescript file.

Keep in mind that as per the config above, we need a package.json or .git in your tree for nvim to see your full project. But it will work on any random .ts file in isolation.

My example:

nvim /tmp/test.ts

To check if LSP is configured successfully, run this command in nvim:

:checkhealth vim.lsp

And you should see the diagnostic like so. In particular look for Active Clients (what is configured for your current buffer) and Enabled Configuration (global available LSP settings):

vim.lsp:                                                                    ✅

- LSP log level : WARN
- Log path: /Users/kt/.local/state/nvim/lsp.log
- Log size: 11 KB

vim.lsp: Active Clients ~
- tsserver (id: 1)
  - Version: ? (no serverInfo.version response)
  - Root directory: nil
  - Command: { "typescript-language-server", "--stdio" }
  - Settings: {}
  - Attached buffers: 1

vim.lsp: Enabled Configurations ~
- tsserver:
  - cmd: { "typescript-language-server", "--stdio" }
  - filetypes: typescript


vim.lsp: File Watcher ~
- file watching "(workspace/didChangeWatchedFiles)" disabled on all clients

vim.lsp: Position Encodings ~
- No buffers contain mixed position encodings

Then go back to the typescript file and type

function testyTest(a: number) {
}

function testyTestEmpty() {
}

tes

And from here trigger your code completion. Default keybinding is C-n. And this is what I see, code completion comes up, and you can make a selection with TAB:

Image showing neovim with the above code snippet, tes, followed by a drop down offering the options testyTest or testyTestEmpty

Diagnostic hints and errors

After you’ve accepted that, you can try something like this:

function testyTest(a: number) {
}

function testyTestEmpty() {
}

testyTest()

Once you exit insert mode, you will see these marks:

image showing the above code snippet with an H next to the function defintion of testyTest and an E next to the invocation of testyTest

This is Neovim’s diagnostic marks - the H indicates a Hint, E indicates Error. The default keybindings are ]d / [d to jump backwards and forwards between them in the buffer, and <C-w>d to shows diagnostic at cursor like so:

image showing details of the diagnostic error on the testyTest() function invocation

See the docs for full diagnostic mark keybindings and of course you can remap them.

Going further

I’ve only just scratched the surface here. I found that there wasn’t a particular single guide to string everything together. So I wrote up what I found.

Check out all the other features available in LSP. Look up general guides on Neovim LSP, they don’t need to be TypeScript specific.

Also look at the excellent Neovim docs, either through their website or through :help lsp / :help diagnostics in-app, these are the parts I referenced here: