feat: refactor lsp configs and drop nvim-cmp

This commit is contained in:
2025-11-15 05:59:28 +01:00
parent ee07734ee8
commit e715992cce
17 changed files with 531 additions and 796 deletions
+114 -24
View File
@@ -1,29 +1,22 @@
---@type fun(name: string, cfg: vim.lsp.Config)
vim.lsp.config = vim.lsp.config
local log = require("log")
local util = require("util")
---@class ow.lsp.Keymap
---@field mode string|string[]
---@field lhs string
---@field rhs string|function
---@field opts? vim.keymap.set.Opts
local M = {}
local Keymap = {}
M.diagnostic_signs = {
text = {
[vim.diagnostic.severity.ERROR] = "E",
[vim.diagnostic.severity.WARN] = "W",
[vim.diagnostic.severity.INFO] = "I",
[vim.diagnostic.severity.HINT] = "H",
},
}
---@param bufnr integer
---@param keymaps ow.lsp.Keymap[]
function Keymap.set(bufnr, keymaps)
for _, keymap in ipairs(keymaps) do
keymap.opts = vim.tbl_extend(
"force",
keymap.opts or {},
{ buffer = bufnr, remap = true }
)
vim.keymap.set(keymap.mode, keymap.lhs, keymap.rhs, keymap.opts)
end
end
---@param bufnr integer
function Keymap.set_defaults(bufnr)
---@type ow.lsp.Keymap[]
local function set_keymaps(bufnr)
local keymaps = {
{ mode = { "n" }, lhs = "<leader>df", rhs = vim.diagnostic.open_float },
{
@@ -63,7 +56,11 @@ function Keymap.set_defaults(bufnr)
lhs = "<C-h>",
rhs = vim.lsp.buf.document_highlight,
},
{ mode = { "n", "x" }, lhs = "<leader>lf", rhs = vim.lsp.buf.format },
{
mode = { "n", "x" },
lhs = "<leader>lf",
rhs = vim.lsp.buf.format,
},
{
mode = { "n" },
lhs = "<leader>ld",
@@ -105,7 +102,100 @@ function Keymap.set_defaults(bufnr)
})
end
Keymap.set(bufnr, keymaps)
for _, keymap in ipairs(keymaps) do
keymap.opts =
vim.tbl_extend("keep", keymap.opts or {}, { buffer = bufnr })
vim.keymap.set(keymap.mode, keymap.lhs, keymap.rhs, keymap.opts)
end
end
return Keymap
--- Load a JSON file and return a parsed table merged with settings
---@param path string
---@param settings? table
---@return table?
local function with_file(path, settings)
local file = io.open(path, "r")
if not file then
return
end
local json = file:read("*all")
file:close()
local ok, resp = pcall(
vim.json.decode,
json,
{ luanil = { object = true, array = true } }
)
if not ok then
log.warning("Failed to parse json file %s: %s", path, resp)
return
end
return vim.tbl_deep_extend("force", settings or {}, resp)
end
function M.on_attach(client, bufnr)
set_keymaps(bufnr)
client.settings = with_file(
string.format(".%s.json", client.name),
client.settings
) or client.settings
if client:supports_method("textDocument/completion") then
vim.lsp.completion.enable(true, client.id, bufnr, {
autotrigger = true,
})
end
end
function M.setup()
vim.diagnostic.config({
underline = true,
signs = M.diagnostic_signs,
virtual_text = false,
float = {
show_header = false,
source = true,
border = "rounded",
focusable = true,
format = function(diagnostic)
return string.format("%s", diagnostic.message)
end,
width = 80,
},
update_in_insert = false,
severity_sort = true,
jump = {
float = true,
wrap = false,
},
})
vim.lsp.enable({
"bashls",
"clangd",
"cmake",
"gopls",
-- "hyprls",
"intelephense",
-- "jedi_language_server",
"lemminx",
"lua_ls",
"mesonlsp",
-- "phpactor",
-- "pyrefly",
"pyright",
"ruff",
"rust_analyzer",
"zls",
})
local capabilities = vim.lsp.protocol.make_client_capabilities()
vim.lsp.config("*", {
capabilities = capabilities,
on_attach = M.on_attach,
})
end
return M
-540
View File
@@ -1,540 +0,0 @@
---@type fun(name: string, cfg: vim.lsp.Config)
vim.lsp.config = vim.lsp.config
local Keymap = require("lsp.keymap")
local Linter = require("lsp.linter")
local log = require("log")
local util = require("util")
local M = {}
M.diagnostic_signs = {
text = {
[vim.diagnostic.severity.ERROR] = "E",
[vim.diagnostic.severity.WARN] = "W",
[vim.diagnostic.severity.INFO] = "I",
[vim.diagnostic.severity.HINT] = "H",
},
}
---@param server string
---@param fn? fun(client: vim.lsp.Client, bufnr: integer)
---@return fun(client: vim.lsp.Client, bufnr: integer)
function M.with_defaults(server, fn)
local default_cb = vim.lsp.config[server].on_attach
return function(client, bufnr)
if default_cb then
default_cb(client, bufnr)
end
Keymap.set_defaults(bufnr)
-- For document highlight
vim.cmd.highlight({ "link LspReferenceRead Visual", bang = true })
vim.cmd.highlight({ "link LspReferenceText Visual", bang = true })
vim.cmd.highlight({ "link LspReferenceWrite Visual", bang = true })
client.settings = M.with_file(
string.format(".%s.json", client.name),
client.settings
) or client.settings
if fn then
fn(client, bufnr)
end
end
end
--- Load a JSON file and return a parsed table merged with settings
---@param path string
---@param settings? table
---@return table?
function M.with_file(path, settings)
local file = io.open(path, "r")
if not file then
return
end
local json = file:read("*all")
file:close()
local ok, resp = pcall(
vim.json.decode,
json,
{ luanil = { object = true, array = true } }
)
if not ok then
log.warning("Failed to parse json file %s: %s", path, resp)
return
end
return vim.tbl_deep_extend("force", settings or {}, resp)
end
function M.setup()
vim.diagnostic.config({
underline = true,
signs = M.diagnostic_signs,
virtual_text = false,
float = {
show_header = false,
source = true,
border = "rounded",
focusable = true,
format = function(diagnostic)
return string.format("%s", diagnostic.message)
end,
width = 80,
},
update_in_insert = false,
severity_sort = true,
jump = {
float = true,
wrap = false,
},
})
vim.lsp.enable({
"bashls",
"clangd",
"cmake",
"gopls",
"hyprls",
"intelephense",
-- "jedi_language_server",
"lemminx",
"lua_ls",
"mesonlsp",
-- "phpactor",
-- "pyrefly",
"pyright",
"ruff",
"rust_analyzer",
"zls",
})
local capabilities = vim.lsp.protocol.make_client_capabilities()
local cmp_nvim_lsp = util.try_require("cmp_nvim_lsp")
if cmp_nvim_lsp then
capabilities = vim.tbl_deep_extend(
"force",
capabilities,
cmp_nvim_lsp.default_capabilities()
)
end
vim.lsp.config("*", {
on_attach = M.with_defaults("*"),
capabilities = capabilities,
})
vim.lsp.config("bashls", {
filetypes = {
"sh",
"bash",
"zsh",
},
on_attach = M.with_defaults("bashls", function(_, bufnr)
Keymap.set(bufnr, {
{
mode = "n",
lhs = "<leader>lf",
rhs = function()
util.format({
buf = bufnr,
cmd = { "shfmt", "-s", "-i", "4", "-" },
output = "stdout",
})
end,
},
})
end),
})
vim.lsp.config("clangd", {
filetypes = {
"c",
"cpp",
},
cmd = {
"clangd",
"--clang-tidy",
"--enable-config",
-- Fix for errors in files outside of project
-- https://clangd.llvm.org/faq#how-do-i-fix-errors-i-get-when-opening-headers-outside-of-my-project-directory
"--compile-commands-dir=build",
},
single_file_support = true,
on_attach = M.with_defaults("clangd", function(_, bufnr)
Linter.add(bufnr, {
cmd = {
"clang-tidy",
"-p=build",
"--quiet",
"--checks=-*,"
.. "clang-analyzer-*,"
.. "-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,"
.. "-clang-analyzer-security.insecureAPI.strcpy",
"%file%",
},
events = { "BufWritePost" },
clear_events = { "TextChanged", "TextChangedI" },
stdin = false,
stdout = true,
pattern = "^.+:(%d+):(%d+): (%w+): (.*) %[(.*)%]$",
groups = { "lnum", "col", "severity", "message", "code" },
source = "clang-tidy",
severity_map = {
error = vim.diagnostic.severity.ERROR,
warning = vim.diagnostic.severity.WARN,
note = vim.diagnostic.severity.HINT,
},
zero_idx_col = true,
zero_idx_lnum = true,
ignore_stderr = true,
})
Keymap.set(bufnr, {
{
mode = "n",
lhs = "gs",
rhs = vim.cmd.LspClangdSwitchSourceHeader,
},
})
end),
})
vim.lsp.config("cmake", {
init_options = {
buildDirectory = "build",
},
})
vim.lsp.config("gopls", {
settings = {
gopls = {
staticcheck = true,
semanticTokens = true,
},
},
on_attach = M.with_defaults("gopls", function(_, bufnr)
Keymap.set(bufnr, {
{
mode = "n",
lhs = "<leader>lf",
rhs = function()
util.format({
buf = bufnr,
cmd = {
"golines",
"-m",
"80",
"--shorten-comments",
},
output = "stdout",
})
vim.lsp.buf.format({ async = true })
end,
},
})
end),
})
vim.lsp.config("intelephense", {
settings = {
intelephense = {
environment = {
phpVersion = "8.4",
},
format = {
enable = true,
braces = "psr12",
},
},
},
on_attach = M.with_defaults("intelephense", function(_, bufnr)
Linter.add(bufnr, {
cmd = {
"phpcs",
"--standard=PSR12",
"--report=emacs",
"-s",
"-q",
"-",
},
stdin = true,
stdout = true,
pattern = "^.+:(%d+):(%d+): (%w+) %- (.*) %((.*)%)$",
groups = { "lnum", "col", "severity", "message", "code" },
source = "phpcs",
severity_map = {
error = vim.diagnostic.severity.ERROR,
warning = vim.diagnostic.severity.WARN,
},
zero_idx_col = true,
zero_idx_lnum = true,
})
Keymap.set(bufnr, {
{
mode = "n",
lhs = "<leader>lf",
rhs = function()
vim.lsp.buf.format()
util.format({
buf = bufnr,
cmd = {
"php-cs-fixer",
"fix",
"%file%",
"--quiet",
},
output = "in_place",
ignore_stderr = true,
env = { PHP_CS_FIXER_IGNORE_ENV = "1" },
})
end,
},
})
end),
})
vim.lsp.config("jedi_language_server", {
init_options = {
completion = {
disableSnippets = true,
},
diagnostics = {
enable = true,
},
},
})
vim.lsp.config("lemminx", {
init_options = {
settings = {
xml = {
format = {
enabled = true, -- is able to format document
splitAttributes = true, -- each attribute is formatted onto new line
joinCDATALines = false, -- normalize content inside CDATA
joinCommentLines = false, -- normalize content inside comments
formatComments = true, -- keep comment in relative position
joinContentLines = false, -- normalize content inside elements
spaceBeforeEmptyCloseLine = true, -- insert whitespace before self closing tag end bracket
},
validation = {
noGrammar = "ignore",
enabled = true,
schema = true,
},
},
},
},
})
local lua_library_paths = {
vim.env.VIMRUNTIME,
}
for _, plugin in ipairs(require("lazy").plugins()) do
table.insert(lua_library_paths, plugin.dir)
end
vim.lsp.config("lua_ls", {
settings = {
Lua = {
completion = { showWord = "Disable" },
runtime = {
version = "LuaJIT",
path = {
"lua/?.lua",
"lua/?/init.lua",
},
pathStrict = true,
},
workspace = {
library = lua_library_paths,
checkThirdParty = false,
},
hint = {
enable = false,
arrayIndex = "Disable",
await = true,
paramName = "All",
paramType = true,
semicolon = "Disable",
setType = true,
},
telemetry = { enable = false },
},
},
on_attach = M.with_defaults("lua_ls", function(_, bufnr)
Keymap.set(bufnr, {
{
mode = "n",
lhs = "<leader>lf",
rhs = function()
util.format({
buf = bufnr,
cmd = {
"stylua",
"--stdin-filepath",
"%file%",
"-",
},
output = "stdout",
auto_indent = true,
})
end,
},
{
mode = "x",
lhs = "<leader>lf",
rhs = function()
util.format({
buf = bufnr,
cmd = {
"stylua",
"--range-start",
"%byte_start%",
"--range-end",
"%byte_end%",
"--stdin-filepath",
"%file%",
"-",
},
output = "stdout",
})
end,
},
})
end),
})
vim.lsp.config("mesonlsp", {
on_attach = M.with_defaults("mesonlsp"),
settings = {
others = {
disableInlayHints = true,
},
},
})
vim.lsp.config("pyright", {
-- Handled in ruff instead
-- on_attach = M.with_defaults("pyright"),
settings = {
python = {
analysis = {
autoSearchPaths = true,
diagnosticMode = "openFilesOnly",
useLibraryCodeForTypes = true,
typeCheckingMode = "strict",
stubPath = "stubs",
},
},
pyright = {
disableLanguageServices = false,
},
},
})
vim.lsp.config("ruff", {
on_attach = M.with_defaults("ruff", function(_, bufnr)
Keymap.set(bufnr, {
{
mode = "n",
lhs = "<leader>lf",
rhs = function()
vim.lsp.buf.format()
util.format({
buf = bufnr,
cmd = {
"ruff",
"check",
"--stdin-filename=%file%",
"--select=I",
"--fix",
"--quiet",
"-",
},
output = "stdout",
})
end,
},
})
end),
})
vim.lsp.config("rust_analyzer", {
on_attach = M.with_defaults("rust_analyzer", function(client)
local handler_name = "textDocument/publishDiagnostics"
local default_handler = client.handlers[handler_name]
or vim.lsp.handlers[handler_name]
client.handlers[handler_name] = function(
err,
result,
context,
config
)
if result and result.diagnostics then
result.diagnostics = vim.tbl_filter(function(diagnostic)
return diagnostic.severity
< vim.diagnostic.severity.HINT
end, result.diagnostics)
end
default_handler(err, result, context, config)
end
end),
settings = {
["rust-analyzer"] = {
check = {
command = "clippy",
extraArgs = {
"--",
"-Wclippy::pedantic",
},
},
diagnostics = {
styleLints = {
enable = true,
},
},
imports = {
prefix = "self",
},
inlayHints = {
chainingHints = {
enable = false,
},
parameterHints = {
enable = false,
},
typeHints = {
enable = false,
},
},
rustfmt = {
extraArgs = { "+nightly" },
},
},
},
})
vim.lsp.config("zls", {
on_attach = M.with_defaults("zls"),
settings = {
zls = {
warn_style = true,
highlight_global_var_declarations = true,
inlay_hints_show_variable_type_hints = false,
inlay_hints_show_struct_literal_field_type = false,
inlay_hints_show_parameter_name = false,
inlay_hints_show_builtin = false,
},
},
})
vim.lsp.config("pyrefly", {})
end
return M
-232
View File
@@ -1,232 +0,0 @@
-- https://github.com/hrsh7th/nvim-cmp
local word_pattern = "[%w_.]"
local function has_words_before()
unpack = unpack or table.unpack
local line, col = unpack(vim.api.nvim_win_get_cursor(0))
return col ~= 0
and vim.api
.nvim_buf_get_lines(0, line - 1, line, true)[1]
:sub(col, col)
:match(word_pattern)
~= nil
end
local function has_words_after()
unpack = unpack or table.unpack
local line, col = unpack(vim.api.nvim_win_get_cursor(0))
return col ~= 0
and vim.api
.nvim_buf_get_lines(0, line - 1, line, true)[1]
:sub(col + 1, col + 1)
:match(word_pattern)
~= nil
end
---@type LazyPluginSpec
return {
"hrsh7th/nvim-cmp",
dependencies = {
"saadparwaiz1/cmp_luasnip",
"hrsh7th/cmp-path",
"hrsh7th/cmp-cmdline",
"hrsh7th/cmp-nvim-lsp",
{
"L3MON4D3/LuaSnip",
config = function()
require("luasnip.loaders.from_vscode").lazy_load()
end,
build = (
require("util").os_name ~= "Windows_NT"
and "make install_jsregexp"
or nil
),
version = "2.*",
dependencies = { "rafamadriz/friendly-snippets" },
},
{
"onsails/lspkind.nvim",
config = function()
require("lspkind").init()
end,
},
"teramako/cmp-cmdline-prompt.nvim",
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
local lspkind = require("lspkind")
---@type cmp.ConfigSchema
local opts = {
-- enabled = function()
-- return has_words_before()
-- end,
preselect = "None",
completion = {
autocomplete = { "InsertEnter", "TextChanged" },
keyword_length = 1,
},
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
---@diagnostic disable-next-line: missing-fields
formatting = {
format = function(entry, vim_item)
vim_item = lspkind.cmp_format({
mode = "symbol",
maxwidth = 50,
ellipsis_char = "...",
before = function(_, item)
item.dup = 0 -- remove duplicates, see nvim-cmp #511
return item
end,
})(entry, vim_item)
return vim_item
end,
},
mapping = {
["<tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item({
behavior = cmp.SelectBehavior.Select,
})
else
fallback()
end
end),
["<S-tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item({
behavior = cmp.SelectBehavior.Select,
})
else
fallback()
end
end),
["<C-n>"] = cmp.mapping.select_next_item({
behavior = cmp.SelectBehavior.Select,
}),
["<C-p>"] = cmp.mapping.select_prev_item({
behavior = cmp.SelectBehavior.Select,
}),
["<CR>"] = cmp.mapping(function(fallback)
if cmp.visible() and cmp.get_active_entry() then
cmp.confirm({
select = false,
behavior = cmp.ConfirmBehavior.Replace,
})
else
fallback()
end
end),
["<C-y>"] = cmp.mapping.confirm({
select = true,
behavior = cmp.ConfirmBehavior.Replace,
}),
["<C-x><C-o>"] = cmp.mapping.complete(),
["<C-l>"] = function(fallback)
if luasnip.locally_jumpable(1) then
luasnip.jump(1)
else
fallback()
end
end,
["<C-h>"] = function(fallback)
if luasnip.locally_jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end,
},
sources = {
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "orgmode" },
{ name = "path" },
},
window = {
completion = cmp.config.window.bordered({
border = "none",
winhighlight = "Normal:Pmenu,CursorLine:PmenuSel,Search:None",
zindex = 1001,
scrolloff = 0,
col_offset = 0,
side_padding = 1,
scrollbar = true,
}),
documentation = cmp.config.window.bordered({
border = "rounded",
winhighlight = "CursorLine:Visual,Search:None",
zindex = 1001,
max_height = 80,
}),
},
}
cmp.setup(opts)
cmp.setup.cmdline("/", {
mapping = cmp.mapping.preset.cmdline(),
sources = { { name = "buffer" } },
})
cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({ { name = "path" } }, {
{
name = "cmdline",
option = { ignore_cmds = { "!" } },
},
}),
})
-- for cmdline `input()` prompt
-- see: `:help getcmdtype()`
cmp.setup.cmdline("@", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{
name = "cmdline-prompt",
---@type prompt.Option
option = {
kinds = {
file = cmp.lsp.CompletionItemKind.File,
dir = {
kind = cmp.lsp.CompletionItemKind.Folder,
hl_group = "CmpItemKindEnum",
},
},
},
},
}),
formatting = {
fields = { "kind", "abbr", "menu" },
format = function(entry, vim_item)
local item = entry.completion_item
if entry.source.name == "cmdline-prompt" then
vim_item.kind = cmp.lsp.CompletionItemKind[item.kind]
local kind =
lspkind.cmp_format({ mode = "symbol_text" })(
entry,
vim_item
)
local strings =
vim.split(kind.kind, "%s", { trimempty = true })
kind.kind = " " .. (strings[1] or "")
kind.menu = " ("
.. (item.data.completion_type or "")
.. ")"
kind.menu_hl_group = kind.kind_hl_group
return kind
else
return vim_item
end
end,
},
})
end,
}