Configuring Neovim’s LSP client for TypeScript development

Life on the edge… of text editing

My terminal experience. I’m using Iosevka as my font and sonokai (default) as my theme.

Installing Neovim + LSP dependencies

The version of Neovim from your package manager probably doesn’t have LSP support yet. As of now, to use the latest LSP features, you need to build Neovim from its master branch. On macOS, this is as simple as running the following command

brew install --HEAD neovim
npm install -g typescript typescript-language-server diagnostic-languageserver eslint_d

nvim-lspconfig

Neovim’s LSP client is built into the program, but configuring it directly can be tricky. That’s where nvim-lspconfig comes in.

Plug 'neovim/nvim-lspconfig'
use "neovim/nvim-lspconfig"

Lua from Vimscript

The rest of the code in this post will be Lua, not Vimscript. To use Lua in your init.vim, you have two options:

lua << EOF(code goes here)EOF
lua require("lsp-config") 
-- or whatever you named your file, relative to your nvim/lua directory

LSP configuration

We’re going to set up two LSP servers: typescript-language-server to wrap around tsserver and analyze your TypeScript code, and diagnostic-languageserver to handle ESLint diagnostics and formatting via Prettier.

local nvim_lsp = require("lspconfig")
nvim_lsp.tsserver.setup {
on_attach = function(client)
client.resolved_capabilities.document_formatting = false
on_attach(client)
end
}
local filetypes = {
typescript = "eslint",
typescriptreact = "eslint",
}
local linters = {
eslint = {
sourceName = "eslint",
command = "eslint_d",
rootPatterns = {".eslintrc.js", "package.json"},
debounce = 100,
args = {"--stdin", "--stdin-filename", "%filepath", "--format", "json"},
parseJson = {
errorsRoot = "[0].messages",
line = "line",
column = "column",
endLine = "endLine",
endColumn = "endColumn",
message = "${message} [${ruleId}]",
security = "severity"
},
securities = {[2] = "error", [1] = "warning"}
}
}
local formatters = {
prettier = {command = "prettier", args = {"--stdin-filepath", "%filepath"}}
}
local formatFiletypes = {
typescript = "prettier",
typescriptreact = "prettier"
}
nvim_lsp.diagnosticls.setup {
on_attach = on_attach,
filetypes = vim.tbl_keys(filetypes),
init_options = {
filetypes = filetypes,
linters = linters,
formatters = formatters,
formatFiletypes = formatFiletypes
}
}
local on_attach = function(client, bufnr)
local buf_map = vim.api.nvim_buf_set_keymap
vim.cmd("command! LspDef lua vim.lsp.buf.definition()")
vim.cmd("command! LspFormatting lua vim.lsp.buf.formatting()")
vim.cmd("command! LspCodeAction lua vim.lsp.buf.code_action()")
vim.cmd("command! LspHover lua vim.lsp.buf.hover()")
vim.cmd("command! LspRename lua vim.lsp.buf.rename()")
vim.cmd("command! LspOrganize lua lsp_organize_imports()")
vim.cmd("command! LspRefs lua vim.lsp.buf.references()")
vim.cmd("command! LspTypeDef lua vim.lsp.buf.type_definition()")
vim.cmd("command! LspImplementation lua vim.lsp.buf.implementation()")
vim.cmd("command! LspDiagPrev lua vim.lsp.diagnostic.goto_prev()")
vim.cmd("command! LspDiagNext lua vim.lsp.diagnostic.goto_next()")
vim.cmd(
"command! LspDiagLine lua vim.lsp.diagnostic.show_line_diagnostics()")
vim.cmd("command! LspSignatureHelp lua vim.lsp.buf.signature_help()")
buf_map(bufnr, "n", "gd", ":LspDef<CR>", {silent = true})
buf_map(bufnr, "n", "gr", ":LspRename<CR>", {silent = true})
buf_map(bufnr, "n", "gR", ":LspRefs<CR>", {silent = true})
buf_map(bufnr, "n", "gy", ":LspTypeDef<CR>", {silent = true})
buf_map(bufnr, "n", "K", ":LspHover<CR>", {silent = true})
buf_map(bufnr, "n", "gs", ":LspOrganize<CR>", {silent = true})
buf_map(bufnr, "n", "[a", ":LspDiagPrev<CR>", {silent = true})
buf_map(bufnr, "n", "]a", ":LspDiagNext<CR>", {silent = true})
buf_map(bufnr, "n", "ga", ":LspCodeAction<CR>", {silent = true})
buf_map(bufnr, "n", "<Leader>a", ":LspDiagLine<CR>", {silent = true})
buf_map(bufnr, "i", "<C-x><C-x>", "<cmd> LspSignatureHelp<CR>",
{silent = true})
if client.resolved_capabilities.document_formatting then
vim.api.nvim_exec([[
augroup LspAutocommands
autocmd! * <buffer>
autocmd BufWritePost <buffer> LspFormatting
augroup END
]], true)
end
end
local format_async = function(err, _, result, _, bufnr)
if err ~= nil or result == nil then return end
if not vim.api.nvim_buf_get_option(bufnr, "modified") then
local view = vim.fn.winsaveview()
vim.lsp.util.apply_text_edits(result, bufnr)
vim.fn.winrestview(view)
if bufnr == vim.api.nvim_get_current_buf() then
vim.api.nvim_command("noautocmd :update")
end
end
end
vim.lsp.handlers["textDocument/formatting"] = format_async
_G.lsp_organize_imports = function()
local params = {
command = "_typescript.organizeImports",
arguments = {vim.api.nvim_buf_get_name(0)},
title = ""
}
vim.lsp.buf.execute_command(params)
end
local nvim_lsp = require("lspconfig")local format_async = function(err, _, result, _, bufnr)
if err ~= nil or result == nil then return end
if not vim.api.nvim_buf_get_option(bufnr, "modified") then
local view = vim.fn.winsaveview()
vim.lsp.util.apply_text_edits(result, bufnr)
vim.fn.winrestview(view)
if bufnr == vim.api.nvim_get_current_buf() then
vim.api.nvim_command("noautocmd :update")
end
end
end
vim.lsp.handlers["textDocument/formatting"] = format_async_G.lsp_organize_imports = function()
local params = {
command = "_typescript.organizeImports",
arguments = {vim.api.nvim_buf_get_name(0)},
title = ""
}
vim.lsp.buf.execute_command(params)
end
local on_attach = function(client, bufnr)
local buf_map = vim.api.nvim_buf_set_keymap
vim.cmd("command! LspDef lua vim.lsp.buf.definition()")
vim.cmd("command! LspFormatting lua vim.lsp.buf.formatting()")
vim.cmd("command! LspCodeAction lua vim.lsp.buf.code_action()")
vim.cmd("command! LspHover lua vim.lsp.buf.hover()")
vim.cmd("command! LspRename lua vim.lsp.buf.rename()")
vim.cmd("command! LspOrganize lua lsp_organize_imports()")
vim.cmd("command! LspRefs lua vim.lsp.buf.references()")
vim.cmd("command! LspTypeDef lua vim.lsp.buf.type_definition()")
vim.cmd("command! LspImplementation lua vim.lsp.buf.implementation()")
vim.cmd("command! LspDiagPrev lua vim.lsp.diagnostic.goto_prev()")
vim.cmd("command! LspDiagNext lua vim.lsp.diagnostic.goto_next()")
vim.cmd(
"command! LspDiagLine lua vim.lsp.diagnostic.show_line_diagnostics()")
vim.cmd("command! LspSignatureHelp lua vim.lsp.buf.signature_help()")
buf_map(bufnr, "n", "gd", ":LspDef<CR>", {silent = true})
buf_map(bufnr, "n", "gr", ":LspRename<CR>", {silent = true})
buf_map(bufnr, "n", "gR", ":LspRefs<CR>", {silent = true})
buf_map(bufnr, "n", "gy", ":LspTypeDef<CR>", {silent = true})
buf_map(bufnr, "n", "K", ":LspHover<CR>", {silent = true})
buf_map(bufnr, "n", "gs", ":LspOrganize<CR>", {silent = true})
buf_map(bufnr, "n", "[a", ":LspDiagPrev<CR>", {silent = true})
buf_map(bufnr, "n", "]a", ":LspDiagNext<CR>", {silent = true})
buf_map(bufnr, "n", "ga", ":LspCodeAction<CR>", {silent = true})
buf_map(bufnr, "n", "<Leader>a", ":LspDiagLine<CR>", {silent = true})
buf_map(bufnr, "i", "<C-x><C-x>", "<cmd> LspSignatureHelp<CR>",
{silent = true})
if client.resolved_capabilities.document_formatting then
vim.api.nvim_exec([[
augroup LspAutocommands
autocmd! * <buffer>
autocmd BufWritePost <buffer> LspFormatting
augroup END
]], true)
end
end
nvim_lsp.tsserver.setup {
on_attach = function(client)
client.resolved_capabilities.document_formatting = false
on_attach(client)
end
}
local filetypes = {
typescript = "eslint",
typescriptreact = "eslint",
}
local linters = {
eslint = {
sourceName = "eslint",
command = "eslint_d",
rootPatterns = {".eslintrc.js", "package.json"},
debounce = 100,
args = {"--stdin", "--stdin-filename", "%filepath", "--format", "json"},
parseJson = {
errorsRoot = "[0].messages",
line = "line",
column = "column",
endLine = "endLine",
endColumn = "endColumn",
message = "${message} [${ruleId}]",
security = "severity"
},
securities = {[2] = "error", [1] = "warning"}
}
}
local formatters = {
prettier = {command = "prettier", args = {"--stdin-filepath", "%filepath"}}
}
local formatFiletypes = {
typescript = "prettier",
typescriptreact = "prettier"
}
nvim_lsp.diagnosticls.setup {
on_attach = on_attach,
filetypes = vim.tbl_keys(filetypes),
init_options = {
filetypes = filetypes,
linters = linters,
formatters = formatters,
formatFiletypes = formatFiletypes
}
}
Ahh, yes, that’s it.

Completion

There’s one more piece to put in place: completion. The built-in LSP provides completion through Vim’s omni completion, but it’s not exactly comfortable if you’re used to the way a modern editor handles completion.

-- use .ts snippets in .tsx files
vim.g.vsnip_filetypes = {
typescriptreact = {"typescript"}
}
require"compe".setup {
preselect = "always",
source = {
path = true,
buffer = true,
vsnip = true,
nvim_lsp = true,
nvim_lua = true
}
}
local t = function(str)
return vim.api.nvim_replace_termcodes(str, true, true, true)
end
_G.tab_complete = function()
if vim.fn.pumvisible() == 1 then
return vim.fn["compe#confirm"]()
elseif vim.fn.call("vsnip#available", {1}) == 1 then
return t("<Plug>(vsnip-expand-or-jump)")
else
return t("<Tab>")
end
end
vim.api.nvim_set_keymap("i", "<Tab>", "v:lua.tab_complete()", {expr = true})
vim.api.nvim_set_keymap("s", "<Tab>", "v:lua.tab_complete()", {expr = true})
vim.api.nvim_set_keymap("i", "<C-Space>", "compe#complete()",
{expr = true, silent = true})
vim.api.nvim_set_keymap("i", "<CR>", [[compe#confirm("<CR>")]],
{expr = true, silent = true})
vim.api.nvim_set_keymap("i", "<C-e>", [[compe#close("<C-e>")]],
{expr = true, silent = true})
The developer is responsive and active on GitHub, too. The Neovim community is great.

Developer, NYC / Tokyo

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store