Skip to main content

Prerequisites

Before starting, ensure you have:
  • Neovim 0.10 or later (nvim --version)
  • A language server installed for your language
  • Basic familiarity with Lua configuration

Install a Language Server

First, install at least one language server. Here are popular options:
# macOS/Linux
brew install lua-language-server

# Or download from GitHub releases
# https://github.com/LuaLS/lua-language-server/releases
See the official server list for more language servers.

Configure and Enable LSP

1

Create or open your init.lua

Create ~/.config/nvim/init.lua if it doesn’t exist:
mkdir -p ~/.config/nvim
nvim ~/.config/nvim/init.lua
2

Define an LSP config

Add a server configuration. Here’s an example for Lua:
vim.lsp.config['lua_ls'] = {
  -- Command to start the server
  cmd = { 'lua-language-server' },
  
  -- Filetypes to attach to automatically
  filetypes = { 'lua' },
  
  -- Workspace root detection
  root_markers = { '.luarc.json', '.luarc.jsonc', '.git' },
  
  -- Server settings
  settings = {
    Lua = {
      runtime = {
        version = 'LuaJIT',
      },
      diagnostics = {
        globals = { 'vim' }, -- Recognize 'vim' global
      },
      workspace = {
        library = vim.api.nvim_get_runtime_file('', true),
        checkThirdParty = false,
      },
      telemetry = {
        enable = false,
      },
    },
  },
}
3

Enable the config

Add this line to activate the LSP:
vim.lsp.enable('lua_ls')
You can also enable multiple servers at once:
vim.lsp.enable({ 'lua_ls', 'ts_ls', 'pyright' })
4

Restart Neovim

Save the file and restart Neovim, or reload the config:
:source $MYVIMRC

Verify It Works

1

Open a code file

Open a file matching your configured filetype:
nvim test.lua
2

Check LSP status

Run the healthcheck to verify LSP is working:
:checkhealth vim.lsp
You should see your server listed under “Enabled Configurations” and “Active Clients”.
3

Test LSP features

Try these default keymaps:
  • Type K over a symbol to see hover documentation
  • Press gd on a variable to go to its definition (via tagfunc)
  • Use CTRL-X CTRL-O in Insert mode for completion
  • Type gra to see available code actions

Complete Example

Here’s a minimal init.lua with multiple language servers:
-- ~/.config/nvim/init.lua

-- Configure Lua language server
vim.lsp.config['lua_ls'] = {
  cmd = { 'lua-language-server' },
  filetypes = { 'lua' },
  root_markers = { '.luarc.json', '.git' },
  settings = {
    Lua = {
      runtime = { version = 'LuaJIT' },
      diagnostics = { globals = { 'vim' } },
      workspace = {
        library = vim.api.nvim_get_runtime_file('', true),
        checkThirdParty = false,
      },
    },
  },
}

-- Configure TypeScript server
vim.lsp.config['ts_ls'] = {
  cmd = { 'typescript-language-server', '--stdio' },
  filetypes = { 'javascript', 'javascriptreact', 'typescript', 'typescriptreact' },
  root_markers = { 'package.json', 'tsconfig.json', '.git' },
}

-- Configure Python server
vim.lsp.config['pyright'] = {
  cmd = { 'pyright-langserver', '--stdio' },
  filetypes = { 'python' },
  root_markers = { 'pyproject.toml', 'setup.py', '.git' },
}

-- Enable all configured servers
vim.lsp.enable({ 'lua_ls', 'ts_ls', 'pyright' })

-- Optional: Add custom keymaps on attach
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local buf = args.buf
    
    -- Go to definition (custom)
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { buffer = buf })
    
    -- Format current buffer
    vim.keymap.set('n', '<leader>f', vim.lsp.buf.format, { buffer = buf })
    
    -- Show line diagnostics
    vim.keymap.set('n', '<leader>d', vim.diagnostic.open_float, { buffer = buf })
  end,
})

-- Optional: Configure diagnostic display
vim.diagnostic.config({
  virtual_text = {
    prefix = '■', -- Could be '●', '▎', 'x'
  },
  signs = true,
  underline = true,
  update_in_insert = false,
  severity_sort = true,
})

Troubleshooting

LSP Not Starting

Ensure the language server is installed and available in your PATH:
which lua-language-server
which typescript-language-server
If not found, install it or specify the full path in cmd:
vim.lsp.config['lua_ls'] = {
  cmd = { '/usr/local/bin/lua-language-server' },
  -- ...
}
LSP servers need a “workspace root” to function. Ensure your project has one of the root_markers:
cd /path/to/your/project
git init  # Creates .git directory
Or create a marker file:
touch .luarc.json
Check that the config is properly defined:
:lua =vim.lsp.config
You should see your config name in the output.

Check Logs

View the LSP log for detailed error messages:
:lua vim.cmd.edit(vim.lsp.log.get_filename())
Or set log level for more verbosity:
-- At the top of init.lua
vim.lsp.log.set_level('debug')

Client Not Attaching

In the problematic buffer, run:
:lua =vim.lsp.get_clients()
If empty, the client isn’t attaching. Check:
  1. The filetype matches your config: :set filetype?
  2. A root marker exists in your project
  3. The server is enabled: :lua =vim.lsp.config['your_server']
Force set the correct filetype:
:set filetype=lua
Or add to your config:
vim.filetype.add({
  extension = {
    conf = 'conf',
    env = 'sh',
  },
})

Using nvim-lspconfig Plugin

If you prefer not to configure servers manually, use the nvim-lspconfig plugin:
-- With lazy.nvim
{
  'neovim/nvim-lspconfig',
  config = function()
    require('lspconfig').lua_ls.setup({
      settings = {
        Lua = {
          diagnostics = { globals = { 'vim' } },
        },
      },
    })
    require('lspconfig').ts_ls.setup({})
    require('lspconfig').pyright.setup({})
  end,
}
nvim-lspconfig provides pre-configured setups for 200+ language servers with sensible defaults.

Next Steps

Now that LSP is working, explore more features:

Configuration

Advanced configuration options

Completion

Set up auto-completion

Diagnostics

Customize error display

Code Actions

Use quick fixes and refactorings

Common Use Cases

Format on Save

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = { '*.lua', '*.ts', '*.js', '*.py' },
  callback = function()
    vim.lsp.buf.format()
  end,
})

Show Diagnostics on Hover

-- Show diagnostics in a floating window when cursor is held
vim.api.nvim_create_autocmd('CursorHold', {
  callback = function()
    vim.diagnostic.open_float(nil, { focusable = false })
  end,
})

-- Reduce updatetime for faster hover
vim.opt.updatetime = 250

Organize Imports on Save

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = { '*.ts', '*.tsx', '*.js', '*.jsx' },
  callback = function()
    vim.lsp.buf.code_action({
      context = { only = { 'source.organizeImports' } },
      apply = true,
    })
  end,
})

Build docs developers (and LLMs) love