-
Notifications
You must be signed in to change notification settings - Fork 2.1k
User contributed tips
We list here some snippets to customize the LSP experience. These are too complex/arbitrary to be enabled by default.
If your language server supports formatting a range of text (as opposed to a whole file), you can use this snippet to map a key to formatting a range with a motion. For example, with this code, gmip
in normal mode will format a paragraph.
function format_range_operator()
local old_func = vim.go.operatorfunc
_G.op_func_formatting = function()
local start = vim.api.nvim_buf_get_mark(0, '[')
local finish = vim.api.nvim_buf_get_mark(0, ']')
vim.lsp.buf.range_formatting({}, start, finish)
vim.go.operatorfunc = old_func
_G.op_func_formatting = nil
end
vim.go.operatorfunc = 'v:lua.op_func_formatting'
vim.api.nvim_feedkeys('g@', 'n', false)
end
vim.api.nvim_set_keymap("n", "gm", "<cmd>lua format_range_operator()<CR>", {noremap = true})
See https://github.com/neovim/neovim/issues/14680 for context.
To open the target of a textDocument/definition
request in a floating window (as in VS Code's "Peek Definition"), you can use the following snippet:
local function preview_location_callback(_, result)
if result == nil or vim.tbl_isempty(result) then
return nil
end
vim.lsp.util.preview_location(result[1])
end
function PeekDefinition()
local params = vim.lsp.util.make_position_params()
return vim.lsp.buf_request(0, 'textDocument/definition', params, preview_location_callback)
end
If the server supports LocationLink
, this will show the full target range (e.g., function body) of the definition. Other requests like textDocument/declaration
can be "peeked" analogously.
nvim-lspconfig offers the ClangdSwitchSourceHeader command by default. It simply replaces the current buffer with the corresponding file. If you'd like to open the corresponding file in a new split/vsplit, you can add such commands easily:
local function switch_source_header_splitcmd(bufnr, splitcmd)
bufnr = require'lspconfig'.util.validate_bufnr(bufnr)
local clangd_client = require'lspconfig'.util.get_active_client_by_name(bufnr, 'clangd')
local params = {uri = vim.uri_from_bufnr(bufnr)}
if clangd_client then
clangd_client.request("textDocument/switchSourceHeader", params, function(err, result)
if err then
error(tostring(err))
end
if not result then
print("Corresponding file can’t be determined")
return
end
vim.api.nvim_command(splitcmd .. " " .. vim.uri_to_fname(result))
end, bufnr)
else
print 'textDocument/switchSourceHeader is not supported by the clangd server active on the current buffer'
end
end
require'lspconfig'.clangd.setup {
-----snip------
commands = {
ClangdSwitchSourceHeader = {
function() switch_source_header_splitcmd(0, "edit") end;
description = "Open source/header in current buffer";
},
ClangdSwitchSourceHeaderVSplit = {
function() switch_source_header_splitcmd(0, "vsplit") end;
description = "Open source/header in a new vsplit";
},
ClangdSwitchSourceHeaderSplit = {
function() switch_source_header_splitcmd(0, "split") end;
description = "Open source/header in a new split";
}
}
}
The commands ClangdSwitchSourceHeader* are available like normal vim commands, that is:
nnoremap <leader>h :ClangdSwitchSourceHeaderVSplit<CR>
The following is an example of integrating with eslint, it uses the .eslinrc.js file to identify the project root, and then uses the instance of eslint available in node_modules:
local util = require "lspconfig".util
require'lspconfig'.diagnosticls.setup{
filetypes = {"javascript", "typescript"},
root_dir = function(fname)
return util.root_pattern("tsconfig.json")(fname) or
util.root_pattern(".eslintrc.js")(fname);
end,
init_options = {
linters = {
eslint = {
command = "./node_modules/.bin/eslint",
rootPatterns = {".eslintrc.js", ".git"},
debounce = 100,
args = {
"--stdin",
"--stdin-filename",
"%filepath",
"--format",
"json"
},
sourceName = "eslint",
parseJson = {
errorsRoot = "[0].messages",
line = "line",
column = "column",
endLine = "endLine",
endColumn = "endColumn",
message = "[eslint] ${message} [${ruleId}]",
security = "severity"
},
securities = {
[2] = "error",
[1] = "warning"
}
},
},
filetypes = {
javascript = "eslint",
typescript = "eslint"
}
}
}
The following is an example of integrating with eslint, it uses the .eslinrc.js file to identify the project root, and then uses the instance of eslint available in node_modules:
local eslint = {
lintCommand = "./node_modules/.bin/eslint -f unix --stdin --stdin-filename ${INPUT}",
lintIgnoreExitCode = true,
lintStdin = true
}
local util = require "lspconfig".util
require "lspconfig".efm.setup {
--cmd = {"efm-langserver",},
init_options = {documentFormatting = true},
filetypes = {"javascript", "typescript"},
root_dir = function(fname)
return util.root_pattern("tsconfig.json")(fname) or
util.root_pattern(".eslintrc.js", ".git")(fname);
end,
settings = {
rootMarkers = {".eslintrc.js", ".git/"},
languages = {
typescript = {eslint}
}
}
}
This example uses eslint_d, which is much faster. Read more here
local eslint = {
lintCommand = "eslint_d -f unix --stdin --stdin-filename ${INPUT}",
lintStdin = true,
lintFormats = {"%f:%l:%c: %m"},
lintIgnoreExitCode = true,
formatCommand = "eslint_d --fix-to-stdout --stdin --stdin-filename=${INPUT}",
formatStdin = true
}
require "lspconfig".efm.setup {
init_options = {documentFormatting = true},
filetypes = {"javascript", "typescript"},
root_dir = function(fname)
return util.root_pattern("tsconfig.json")(fname) or
util.root_pattern(".eslintrc.js", ".git")(fname);
end,
settings = {
rootMarkers = {".eslintrc.js", ".git/"},
languages = {
javascript = {eslint},
typescript = {eslint}
}
}
}
It can be helpful to customize the severity levels that get displayed as a gutter sign and/or codelens virtual text. For example, maybe you want to show everything in the gutter, but only Errors as virtualtext.
-- Set which codelens text levels to show
local original_set_virtual_text = vim.lsp.diagnostic.set_virtual_text
local set_virtual_text_custom = function(diagnostics, bufnr, client_id, sign_ns, opts)
opts = opts or {}
-- show all messages that are Warning and above (Warning, Error)
opts.severity_limit = "Warning"
original_set_virtual_text(diagnostics, bufnr, client_id, sign_ns, opts)
end
vim.lsp.diagnostic.set_virtual_text = set_virtual_text_custom
Or maybe you want to only show Errors in the gutter,
local orig_set_signs = vim.lsp.diagnostic.set_signs
local set_signs_limited = function(diagnostics, bufnr, client_id, sign_ns, opts)
opts = opts or {}
opts.severity_limit = "Error"
orig_set_signs(diagnostics, bufnr, client_id, sign_ns, opts)
end
vim.lsp.diagnostic.set_signs = set_signs_limited
Another configuration that only shows the most severe item in the gutter per line,
-- Capture real implementation of function that sets signs
local orig_set_signs = vim.lsp.diagnostic.set_signs
local set_signs_limited = function(diagnostics, bufnr, client_id, sign_ns, opts)
-- original func runs some checks, which I think is worth doing
-- but maybe overkill
if not diagnostics then
diagnostics = diagnostic_cache[bufnr][client_id]
end
-- early escape
if not diagnostics then
return
end
-- Work out max severity diagnostic per line
local max_severity_per_line = {}
for _,d in pairs(diagnostics) do
if max_severity_per_line[d.range.start.line] then
local current_d = max_severity_per_line[d.range.start.line]
if d.severity < current_d.severity then
max_severity_per_line[d.range.start.line] = d
end
else
max_severity_per_line[d.range.start.line] = d
end
end
-- map to list
local filtered_diagnostics = {}
for i,v in pairs(max_severity_per_line) do
table.insert(filtered_diagnostics, v)
end
-- call original function
orig_set_signs(filtered_diagnostics, bufnr, client_id, sign_ns, opts)
end
vim.lsp.diagnostic.set_signs = set_signs_limited
After neovim 0.7, just need to use vim.diagnostic.config()
to set how diagnostics are displayed in virtual text, float, and signs. For example
vim.diagnostic.config({
virtual_text = {
source = 'always',
prefix = '■',
-- Only show virtual text matching the given severity
severity = {
-- Specify a range of severities
min = vim.diagnostic.severity.ERROR,
},
},
float = {
source = 'always',
border = 'rounded',
},
signs = true,
underline = true,
update_in_insert = false,
severity_sort = true,
})
The SchemaStore project provides a large collection of JSON schema definitions for many common JSON file types. The SchemaStore.nvim plugin provides access to the SchemaStore catalog as a Lua library that can be used to configure jsonls.
To use the entire SchemaStore catalog with jsonls, install the SchemaStore.nvim plugin, then update your lspconfig jsonls settings:
To use SchemaStore.nvim with lspconfig + jsonls:
require('lspconfig').jsonls.setup {
settings = {
json = {
schemas = require('schemastore').json.schemas(),
validate = { enable = true },
},
},
}
For an explanation of why the validate = { enable = true }
option is recommended, see SchemaStore.nvim#8.
To use a subset of the catalog, you can select schemas by name (see the catalog for a full list):
require('lspconfig').jsonls.setup {
settings = {
json = {
schemas = require('schemastore').json.schemas {
select = {
'.eslintrc',
'package.json',
},
},
validate = { enable = true },
},
},
}
To ignore certain schemas from the catalog:
require('lspconfig').jsonls.setup {
settings = {
json = {
schemas = require('schemastore').json.schemas {
ignore = {
'.eslintrc',
'package.json',
},
},
validate = { enable = true },
},
},
}
To replace certain schemas from the catalog with your own:
require('lspconfig').jsonls.setup {
settings = {
json = {
schemas = require('schemastore').json.schemas {
replace = {
['package.json'] = {
description = 'package.json overriden',
fileMatch = { 'package.json' },
name = 'package.json',
url = 'https://example.com/package.json',
},
},
},
validate = { enable = true },
},
},
}
If you want to use your own schemas in addition to schemas from SchemaStore, you can merge them:
require('lspconfig').jsonls.setup {
settings = {
json = {
schemas = vim.list_extend(
{
{
description = 'My Custom JSON schema',
fileMatch = { 'foobar.json', '.foobar.json' },
name = 'foobar.json',
url = 'https://example.com/schema/foobar.json',
},
},
require('schemastore').json.schemas {
select = {
'.eslintrc',
'package.json',
},
}
),
validate = { enable = true },
},
},
}
To add the user dictionary from the built-in Neovim spell checker to ltex-ls
, add the following to your LSP setup:
-- ===========================================
-- Add user dictionary for ltex-ls
-- * en.utf-8.add must be created using `zg`
-- ===========================================
local path = vim.fn.stdpath 'config' .. '/spell/en.utf-8.add'
local words = {}
for word in io.open(path, 'r'):lines() do
table.insert(words, word)
end
nvim_lsp.ltex.setup {
on_attach = on_attach,
settings = {
ltex = {
dictionary = {
['en-US'] = words,
},
},
},
}
The changes take places when you reload the config.
The nvim-notify plugin can be used to display messages received from the LSP server.
vim.lsp.handlers['window/showMessage'] = function(_, result, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
local lvl = ({ 'ERROR', 'WARN', 'INFO', 'DEBUG' })[result.type]
notify(result.message, lvl, {
title = 'LSP | ' .. client.name,
timeout = 10000,
keep = function()
return lvl == 'ERROR' or lvl == 'WARN'
end,
})
end