quarry.nvim is a wrapper for mason, mason-lspconfig, and lspconfig to orgnize LSP setup for Neovim.
Having multiple LSP can easily bloat your single-file configuration. You still want to manage LSP yourself so you still understand what is going on. So you need a simple way to split your setup into multiple files.
- Composable LSP configuration
- Lazy LSP installation only when required to keep your Neovim blazingly fast
- Additional tool installation without fuzz, ex. DAP, linter, formatter (
⚠️ configuration of those is still with you) - Configures minimal LSP capabilities and server setup
- it is not a swiss army knife solution with with a one-line setup to automagically manage all your Neovim LSP, DAP, formatter, and linter needs.
Important
If you are not interested in managing your own configuration, then you better head over to none-ls or lsp-zero.
Neovim>= 0.10.0williamboman/mason.nvimwilliamboman/mason-lspconfig.nvimneovim/nvim-lspconfig>= 1.0.0 (optional for NeoVim 0.11+)
| NeoVim Version | nvim-lspconfig | Notes |
|---|---|---|
| 0.10.x | Required | Uses legacy lspconfig API |
| 0.11+ | Optional | Can use native vim.lsp.config() API |
For NeoVim 0.11+, quarry.nvim automatically detects and uses the native LSP configuration API (vim.lsp.config and vim.lsp.enable). You can still use nvim-lspconfig if you prefer, and quarry.nvim will work seamlessly with both approaches.
With lazy.nvim
return {
{
"rsmdt/quarry.nvim",
dependencies = {
"williamboman/mason.nvim",
"williamboman/mason-lspconfig.nvim",
-- Optional for NeoVim 0.11+: quarry.nvim automatically uses the native
-- vim.lsp.config() API when available. For NeoVim 0.10, lspconfig is required.
-- You can also override with your own implementation (see examples below).
"neovim/nvim-lspconfig"
},
}
}
Note
The below shows the default configuration and you do not need to provide this explicitly.
require("quarry").setup({
---
-- Define the features to be enabled (if supported) when the LSP attaches
--
-- Possible values are:
-- "textDocument/documentHighlight",
-- "textDocument/inlayHint",
-- "textDocument/codeLens",
features = {},
---
-- Define the keymaps to be set for the buffer when the LSP attaches. The
-- syntax is similar to Lazy nvim.
--
-- Examples:
-- ["[d"] = { vim.diagnostic.goto_prev },
-- ["]d"] = { vim.diagnostic.goto_next },
-- ["K"] = { vim.lsp.buf.hover, desc = "Show lsp hover" },
keys = {}
---
-- Will be passed when the LSP attaches. Alternatively, use `LspAttach` event.
-- You can manually manage LSP features or keymaps in here as well.
on_attach = function(client, bufnr) end,
---
-- will be passed to every LSP. Can also be defined as Lua table, ex. `capabilities = {}`
capabilities = function()
return vim.tbl_deep_extend("force", {}, vim.lsp.protocol.make_client_capabilities())
end,
---
-- Provide globally required mason tools; will be installed upon `require("quarry").setup()`
tools = {},
---
-- Provide specific LSP configuration here. Every config can have the following shape:
--
-- servers = {
-- lua_ls = {
-- -- Specify the filetypes when to install the tools
-- ---@type string[]
-- filetypes = { "lua" }, -- optional
-- -- List of tools to install for the server
-- ---@type string[]
-- tools = { "lua_ls", "stylua", "luacheck" }, -- LSP itself needs to be specified
-- -- The LSP-specific options
-- ---@type table<any, any>
-- config = {
-- settings = { telemetry = { enable = false } } ,
-- },
-- }
-- }
servers = {},
---
-- Provide LSP-specific handler functions or override the default. A setup handler with `_`
-- as the key will be used as default if no LSP-specific one is defined.
--
-- The default handler automatically detects and uses:
-- - NeoVim 0.11+ native API (vim.lsp.config + vim.lsp.enable) when available
-- - Legacy lspconfig API for NeoVim 0.10 or when explicitly using lspconfig
setup = {
_ = function(name, opts)
-- NeoVim 0.11+ native API
if vim.lsp.config and vim.lsp.enable then
vim.lsp.config(name, opts)
vim.lsp.enable(name)
return
end
-- Fallback to lspconfig
local ok, lspconfig = pcall(require, "lspconfig")
if ok then
lspconfig[name].setup(opts)
end
end
}
})🚀 Composable configuration (best enjoyed with lazy.nvim)
When you use many LSP, your configuration table may become quite large. You can take advantage of a lazy.nvim behaviour and separate the LSP into different files. lazy.nvim merges the opts in case a plugin is defined multiple times.
Tip
on_attach and capabilities are optional. For details see :h lspconfig-configurations inside Neovim. Both settings are totally optional.
Note
You can tweak the below example however you like. I found it most simple for the majority of purposes.
inside your Neovim configuration directory, you will have:
lua/plugins/lsp.luaas your base setuplua/plugins/extras/lua.luafor Lua LSP specific configurationlua/plugins/extras/typescript.luafor Typescript LSP specific configuration- ...extend with other LSP as you like
Setup quarry.nvim in lua/plugins/lsp.lua
-- file: lua/plugins/quarry.lua
return {
"rsmdt/quarry.nvim",
event = "VeryLazy",
dependencies = {
"williamboman/mason.nvim",
"williamboman/mason-lspconfig.nvim",
"neovim/nvim-lspconfig",
-- not required by quarry.nvim, just to show how to extend capabilities
"hrsh7th/cmp-nvim-lsp",
---
-- This takes advantage of lazy.nvim loading mechanism and makes Lazy aware to
-- load modules from within /lua/plugins/extras/*
--
-- Alternatively, you can add this to lua/init.lua:
--
-- -- ... require lazy.nvim as you usually would. Check out the documentation for detailed instructions ...
-- require("lazy").setup({
-- { import = "plugins" },
-- { import = "extras" }, -- <- this is the relevant line, BTW
-- }, {
-- -- .. regular lazy.nvim configuration ...
-- })
{ import = "plugins.extras" },
},
config = {
features = {
"textDocument/documentHighlight",
"textDocument/inlayHint",
-- "textDocument/codeLens",
},
keys = {
["[d"] = { vim.diagnostic.goto_prev },
["]d"] = { vim.diagnostic.goto_next },
["K"] = { vim.lsp.buf.hover, desc = "Show lsp hover" },
["gD"] = { vim.lsp.buf.declaration, desc = "[G]oto [D]eclaration" },
["gs"] = { vim.lsp.buf.signature_help, desc = "[G]oto [s]ignature" },
["gd"] = { vim.lsp.buf.definition, desc = "[G]oto [d]efinition" },
["gr"] = { vim.lsp.buf.references, desc = "[G]oto [r]eferences" },
["gi"] = { vim.lsp.buf.implementation, desc = "[G]oto [i]mplementation" },
["gt"] = { vim.lsp.buf.type_definition, desc = "Goto [t]ype definition" },
["<leader>a"] = { vim.lsp.buf.code_action, desc = "Code [a]ction" },
["<leader>r"] = { vim.lsp.buf.rename, desc = "[R]ename word under cursor within project" },
["<leader>h"] = {
function()
vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
end,
desc = "Toggle inlay [h]int",
},
-- vim.api.nvim_command('inoremap <C-space> <C-x><C-o>')
["<C-space>"] = { "<C-x><C-o>", mode = "i", remap = false },
}
on_attach = function(client, bufnr)
-- Enable completion triggered by <c-x><c-o>
vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"
end,
capabilities = function()
local cmp_nvim_lsp = require("hrsh7th/cmp-nvim-lsp")
return vim.tbl_deep_extend(
"force",
{},
vim.lsp.protocol.make_client_capabilities(),
cmp_nvim_lsp.default_capabilities()
)
end,
},
}
Setup lua-language-server in lua/plugins/extras/lua.lua
-- file: lua/plugins/extras/lua.lua
return {
"rsmdt/quarry.nvim",
config = {
servers = {
lua_ls = {
tools = {
"lua_ls",
"stylua",
"luacheck",
-- only install when opening a file associated with lua_ls
-- this can be set as `filetypes` or will be automatically
-- taken from lspconfig
lazy = true,
},
config = {
settings = {
Lua = {
completion = { callSnippet = "Replace" },
doc = { privateName = { "^_" } },
codeLens = { enable = true },
hint = {
enable = true,
setType = false,
paramType = true,
paramName = "Disable",
semicolon = "Disable",
arrayIndex = "Disable",
},
workspace = {
checkThirdParty = false,
},
},
-- Do not send telemetry data containing a randomized but unique identifier
telemetry = { enable = false },
},
},
},
},
},
}Setup typescript-language-server in lua/plugins/extras/typescript.lua
-- file: lua/plugins/extras/typescript.lua
return {
"rsmdt/quarry.nvim",
config = {
servers = {
ts_ls = {
tools = {
"ts_ls",
"prettier", -- prettierd as alternative
"eslint", -- eslint_d as alternative
-- only install when file associated with 'ts_ls' is opened
lazy = true,
},
config = {
completions = { completeFunctionCalls = true },
init_options = {
preferences = {
includeInlayParameterNameHints = "all",
includeInlayParameterNameHintsWhenArgumentMatchesName = false,
includeInlayFunctionParameterTypeHints = true,
includeInlayVariableTypeHints = true,
includeInlayPropertyDeclarationTypeHints = true,
includeInlayFunctionLikeReturnTypeHints = true,
includeInlayEnumMemberValueHints = true,
importModuleSpecifierPreference = "non-relative",
},
},
},
},
},
},
}
- Neovim >= 0.10.0
- plenary.nvim (for testing)
- Lua 5.1+ (for syntax checking)
quarry.nvim includes a comprehensive test suite to ensure compatibility across NeoVim versions.
Run the implementation verification script:
nvim --headless -u tests/verify_implementation.luaThis will verify:
- Version detection module
- Setup handler logic
- Compatibility with your NeoVim version
# Run all tests
make test
# Run tests in isolation (with minimal init)
make isolated
# Run tests with coverage (requires luacov)
make coverage
# Format code with stylua
make formatUsing plenary.nvim:
# Run all tests
nvim --headless --noplugin -u tests/setup.lua \
-c "lua require('plenary.busted').run('tests/')" \
-c "qa"
# Run specific test file
nvim --headless --noplugin -u tests/setup.lua \
-c "lua require('plenary.busted').run('tests/version_spec.lua')" \
-c "qa"tests/
├── setup.lua # Minimal test environment
├── verify_implementation.lua # Comprehensive verification
├── version_spec.lua # Version detection tests
├── config_spec.lua # Config module tests
└── installer_spec.lua # Installer module tests
Check Lua syntax for all modules:
# Check all files
for file in lua/quarry/*.lua; do
luac -p "$file" && echo "✓ $(basename $file) OK"
done
# Or check a specific file
luac -p lua/quarry/config.lua# Format code with stylua (if installed)
stylua lua/ tests/
# Check formatting without modifying
stylua --check lua/ tests/quarry.nvim/
├── lua/
│ └── quarry/
│ ├── config.lua # Main configuration and setup
│ ├── installer.lua # Mason tool installation
│ ├── features.lua # LSP feature management
│ ├── keymaps.lua # LSP keymap setup
│ ├── utils.lua # Utility functions
│ └── version.lua # Version detection (0.11+ compat)
├── tests/
│ ├── setup.lua # Test environment
│ ├── verify_implementation.lua
│ ├── version_spec.lua
│ ├── config_spec.lua
│ └── installer_spec.lua
├── docs/
│ └── analysis/ # Implementation analysis docs
├── README.md
└── Makefile
quarry.nvim automatically detects and adapts to your NeoVim version:
- NeoVim 0.10.x: Uses legacy
lspconfigAPI - NeoVim 0.11+: Uses native
vim.lsp.config()andvim.lsp.enable()APIs
You can check the detected configuration method:
local version = require("quarry.version")
print(version.get_lsp_config_method()) -- "native" or "lspconfig"
print(vim.inspect(version.get_debug_info()))When contributing to quarry.nvim:
-
Use conventional commits for automatic versioning:
feat:- New featuresfix:- Bug fixesdocs:- Documentation changestest:- Test additions or modificationsrefactor:- Code refactoringchore:- Maintenance tasks
-
Run tests before submitting:
make test nvim --headless -u tests/verify_implementation.lua -
Check syntax:
luac -p lua/quarry/*.lua -
Update documentation if adding features or changing APIs
-
Maintain backwards compatibility - avoid breaking changes in minor versions
For maintainers publishing to luarocks:
- Refer to
neorocksfor the publishing workflow - Use conventional commits to trigger automatic versioning
- Ensure all tests pass before tagging a release